std::线程管理:用法和最佳实践



在 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只会帮助链接,而不是编译。