在 Java 中使用线程后,我试图找出线程,我有点困惑。两个问题:
- 我可以从线程
- 扩展我的类,还是必须通过处理程序从类内管理线程?
- 如何保存所述线程处理程序? std::thread本身似乎没有命名类型。
任何朝着正确方向的推动将不胜感激。
如何解释此消息?
src/CHandler.h:27:9: error: 'thread' in namespace 'std' does not name a type
std::thread _thread;
^
这是我扩展线程的尝试:
src/CHandler.h:17:30: error: expected class-name before '{' token
class CHandler : std::thread {
^
完整、麻烦的标头:
#ifndef __projectm__CHandler__
#define __projectm__CHandler__
#include <set>
#include <vector>
#include <thread>
#include "CListener.h"
class CHandler {
public:
virtual bool subscribe(std::shared_ptr<CListener> aListener);
virtual bool unsubscribe(std::shared_ptr<CListener> aListener);
virtual bool hasSubscriber(std::shared_ptr<CListener> aListener);
virtual ~CHandler() {}
protected:
std::thread _thread;
std::vector<std::weak_ptr<CListener> > _subscribers;
std::set<const CListener *> _subscribersSet;
virtual void run();
};
#endif /* defined(__projectm__CDefaultHandler__) */
编译器版本:
bash-3.1$ g++ --version
g++.exe (GCC) 4.8.1
制作文件(一团糟,我知道 - 仍在学习这种血腥的东西):
CC=g++
OUTFILE=game
BINDIR=bin
SRCDIR=src
OBJDIR=obj
CFLAGS=
LDFLAGS=-std=c++0x
all: core
# Ядро проекта.
core: $(OBJDIR)/main.o $(OBJDIR)/CGame.o $(OBJDIR)/CHandler.o $(OBJDIR)/CListener.o
$(CC) $(CFLAGS) $(wildcard $(OBJDIR)/*.o) -o $(BINDIR)/$(OUTFILE)
$(OBJDIR)/main.o: $(OBJDIR)
$(CC) $(LDFLAGS) $(SRCDIR)/main.cpp -c -o $(OBJDIR)/main.o
$(OBJDIR)/CGame.o: $(OBJDIR)
$(CC) $(LDFLAGS) $(SRCDIR)/CGame.cpp -c -o $(OBJDIR)/CGame.o
$(OBJDIR)/CHandler.o: $(OBJDIR)
$(CC) $(LDFLAGS) $(SRCDIR)/CHandler.cpp -c -o $(OBJDIR)/CHandler.o
$(OBJDIR)/CListener.o: $(OBJDIR)
$(CC) $(LDFLAGS) $(SRCDIR)/CListener.cpp -c -o $(OBJDIR)/CListener.o
# Создаем директорию для объектов, если ее нет.
$(OBJDIR):
mkdir $(OBJDIR)
main.o: $(SRC)/main.cpp
使用 std::thread
作为未经修饰的局部变量的问题之一是它不是异常安全的。 我承认,在展示小HelloWorlds时,我自己也经常对此感到内疚。
确切地知道您要进入的内容,因此以下是使用std::thread
的异常安全方面的更详细说明:
#include <iostream>
#include <thread>
void f() {}
void g() {throw 1;}
int
main()
{
try
{
std::thread t1{f};
g();
t1.join();
}
catch (...)
{
std::cout << "unexpected exception caughtn";
}
}
在上面的例子中,我有一个"大"程序,它"偶尔"会抛出异常。 通常,我想在异常冒泡到main
之前捕获并处理异常。 然而,作为最后的手段,main
本身就被包裹在了万能的尝试中。 在这个例子中,我只是简单地打印出发生了一些非常糟糕的事情并退出。 在更现实的示例中,您可能会让您的客户端有机会保存工作,释放内存或磁盘空间,启动提交错误报告的不同进程等。
看起来不错,对吧? 不幸的是错了。 运行此命令时,输出为:
libc++abi.dylib: terminating
Abort trap: 6
在正常返回之前,我没有向我的客户发出出现问题的通知main
。 我期待这个输出:
unexpected exception caught
相反,std::terminate()
被召唤了。
为什么?
事实证明,~thread()
看起来像这样:
thread::~thread()
{
if (joinable())
terminate();
}
因此,当g()
投掷时,t1.~thread()
在堆栈展开期间运行,并且没有t1.join()
被调用。 因此t1.~thread()
调用std::terminate()
.
不要问我为什么。 这是一个很长的故事,我缺乏客观性来公正地讲述它。
无论如何,您必须了解这种行为,并防止它。
一种可能的解决方案是回到包装器设计,也许使用 OP 首先提出并在其他答案中警告的私有继承:
class CHandler
: private std::thread
{
public:
using std::thread::thread;
CHandler() = default;
CHandler(CHandler&&) = default;
CHandler& operator=(CHandler&&) = default;
~CHandler()
{
if (joinable())
join(); // or detach() if you prefer
}
CHandler(std::thread t) : std::thread(std::move(t)) {}
using std::thread::join;
using std::thread::detach;
using std::thread::joinable;
using std::thread::get_id;
using std::thread::hardware_concurrency;
void swap(CHandler& x) {std::thread::swap(x);}
};
inline void swap(CHandler& x, CHandler& y) {x.swap(y);}
目的是创建一个新类型,比如说CHandler
,它的行为就像一个std::thread
,除了它的析构函数。 ~CHandler()
应在其析构函数中调用join()
或detach()
。 我在上面选择了join()
。 现在,可以在我的示例代码中简单地用CHandler
代替std::thread
:
int
main()
{
try
{
CHandler t1{f};
g();
t1.join();
}
catch (...)
{
std::cout << "unexpected exception caughtn";
}
}
输出现在为:
unexpected exception caught
如预期的那样。
为什么选择join()
而不是detach()
~CHandler()
?
如果使用 join()
,则主线程的堆栈展开将阻塞,直到f()
完成。 这可能是你想要的,也可能不是。 我无法为您回答这个问题。 只有您可以为您的应用程序决定此设计问题。 考虑:
// simulate a long running thread
void f() {std::this_thread::sleep_for(std::chrono::minutes(10));}
main()
线程在 g()
下仍会抛出异常,但现在在展开过程中会挂起,仅 10 分钟后打印出来:
unexpected exception caught
并退出。 也许是因为 f()
中使用的引用或资源,这是您需要发生的事情。 但如果不是,那么您可以改为:
~CHandler()
{
if (joinable())
detach();
}
然后你的程序将立即输出"捕获意外异常"并返回,即使f()
仍然忙于处理(在返回main()
后f()
将被强制取消作为应用程序正常关闭的一部分)。
也许您需要为某些线程join()-on-unwinding
,而为其他线程detach()-on-unwinding
。 也许这会引导您使用两个类似CHandler
的包装器,或者一个基于策略的包装器。 委员会无法就解决方案达成共识,因此您必须决定什么适合您,或者与terminate()
一起生活。
这直接利用了std::thread
非常非常低级的行为。 对于 Hello World,还可以,但在实际应用程序中,最好通过私有继承或作为私有数据成员封装在中级处理程序中。 好消息是,在 C++11 中,中级处理程序现在可以可移植地编写(在 std::thread
之上),而不是像 C++98/03 中那样写到操作系统或第三方库。
建议不要从std::thread
继承:无论如何它没有virtual
方法。我什至建议不要使用构图。
std::thread
的主要问题是它会在构建线程后立即启动(除非您使用其默认构造函数)。因此,许多情况充满了危险:
// BAD: Inheritance
class Derived: std::thread {
public:
Derived(): std::thread(&Derived::go, this), _message("Hello, World!") {}
void go() const { std::cout << _message << std::endl; }
private:
std::string _message;
};
线程可能会在构建_message
之前执行go
,从而导致数据争用。
// BAD: First Attribute
class FirstAttribute {
public:
FirstAttribute(): _thread(&Derived::go, this), _message("Hello, World!") {}
void go() const { std::cout << _message << std::endl; }
private:
std::thread _thread;
std::string _message;
};
同样的问题,线程可能会在构建_message
之前执行go
,从而导致数据争用。
// BAD: Composition
class Safer {
public:
virtual void go() const = 0;
protected:
Safer(): _thread(&Derived::go, this) {}
private:
std::thread _thread;
};
class Derived: Safer {
virtual void go() const { std::cout << "Hello, World!n"; }
};
同样的问题,线程可能会在构建Derived
之前执行go
,从而导致数据争用。
如您所见,无论是继承还是组合,都很容易在不知不觉中引起数据竞争。使用 std::thread
作为类的最后一个属性将起作用......如果你能确保没有人从这个类派生。
因此,目前对我来说,建议仅使用 std::thread
作为局部变量似乎更好。请注意,如果您使用async
设施,您甚至不必自己管理std::thread
。
Bjarne Stroustrup在他的C++11常见问题解答中展示了一些使用std::thread
的例子。最简单的示例如下所示:
#include<thread>
void f();
struct F {
void operator()();
};
int main()
{
std::thread t1{f}; // f() executes in separate thread
std::thread t2{F()}; // F()() executes in separate thread
}
通常,std::thread
不打算从中继承。传递一个函数以在构造函数中异步执行。
如果您的编译器不支持 std::thread
,则可以改用Boost.Thread
。它是相当兼容的,所以一旦编译器支持它,你就可以通过std::thread
替换它。
首先,您使用的是什么编译器和编译器版本?std::thread是相当新的,直到最近才在其中一些中实现。这可能是你的问题。
其次你做了
#include <thread>
第三(这不是你的直接问题)这不是如何在 c++ 中使用线程。您不会从它继承,而是创建一个实例,传入您希望它运行的函数。
std::thread mythread = std::thread(my_func);
(不过,您可以传入的不仅仅是一个简单的函数)
确保在编译和链接时,使用:
g++ -std=c++11 your_file.cpp -o your_program
弄乱LDFLAGS只会帮助链接,而不是编译。