std::thread 管理:使用和最佳实践

5
我正在尝试了解Java中的线程,但有些困惑。 以下是两个问题:
  • 我可以从线程扩展我的类,还是必须通过处理程序在类内管理线程?
  • 如何保存所述线程处理程序?std :: thread本身似乎没有命名类型。
如果能给个指引就好了。
请注意,以上内容保留了HTML标签。
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

Makefile(一团糟,我知道 - 仍在学习这个该死的东西):

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

3
std::thread 是一个类。我不明白你的问题。 - Björn Pollex
你应该发布完整的代码。你可能忘记了包含适当的头文件。 - Björn Pollex
@juanchopanza,无论我是否继承,我仍然会得到“错误:名称空间'std'中的'thread'不是一种类型”的错误。 - Maxim Kumpan
1
这并不是问题,但是包含两个连续下划线(__projectm__CHandler__)的名称以及以下划线和大写字母开头的名称都是保留给实现的。请不要使用它们。 - Pete Becker
@PeteBecker,感谢您的提示。那是我合作伙伴提交的一部分。我会和他谈谈这个问题。 - Maxim Kumpan
显示剩余6条评论
5个回答

10

在使用未装饰的本地变量std::thread时,存在一个问题,即它不是异常安全的。我承认,在演示小型HelloWorld时,我经常会犯这个错误。

然而,了解使用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 caught\n";
    }
}

在上面的例子中,我有一个“大型”程序,它“偶尔”会抛出异常。通常情况下,我希望在异常传递到main之前捕获和处理异常。然而作为最后的手段,main本身被包装在try-catch-all中。在这个例子中,我只是打印出发生了一些非常糟糕的事情并退出。在更现实的例子中,您可能会给客户保存工作的机会,或者释放内存或磁盘空间,启动一个不同的进程来提交错误报告等等。
看起来不错,对吗?不幸的是,错了。运行时输出如下:
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()。现在,可以在我的示例代码中简单地将std::thread替换为CHandler

int
main()
{
    try
    {
        CHandler t1{f};
        g();
        t1.join();
    }
    catch (...)
    {
        std::cout << "unexpected exception caught\n";
    }
}

现在的输出结果为:

unexpected exception caught

按预期执行。

为什么在 ~CHandler()中选择使用join()而不是detach()

如果您使用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中那样编写到操作系统或第三方库。


9
建议不要继承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

6

Bjarne Stroustrup在他的C++11 FAQ中展示了使用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 _thread; 返回 error: 'thread' in namespace 'std' does not name a type - Maxim Kumpan
这很可能意味着你的一些包含文件出了问题。如果没有完整的源代码,就无法诊断错误。你尝试编译我发布的示例了吗?如果它可以工作,那么你就知道不是编译器的问题。你应该阅读一些关于C++中包含文件的工作原理的资料,因为它们与Java中的包导入非常不同。 - Björn Pollex
好问题。我会尝试编译那个例子,看看线程本身是否能够工作。 - Maxim Kumpan
不,它不起作用。g++ -std=c++0x -o test test.cpp \\ test.cpp:在函数'int main()'中:\\ test.cpp:11:5:错误:'thread'不是'std'的成员 - Maxim Kumpan
@MaximKumpan:我已经更新了我的答案。作为一种解决方法,您可以使用Boost。 - Björn Pollex
1
没有什么比临时解决方案更持久。但我认为Boost作为传统的线程解决方案不会有问题。我会尝试一下。谢谢帮助。 - Maxim Kumpan

0
首先,你使用的编译器和编译器版本是什么? std::thread 相对较新,在一些编译器中直到最近才实现。这可能是你的问题。
其次,你是否...
#include <thread> 

第三,这并不是你当前的问题,这不是在C++中使用线程的方式。你不需要继承它,而是创建一个实例,并传入你想要运行的函数。
std::thread mythread = std::thread(my_func);

你可以传入比简单函数更多的东西


@MaximKumpan 你使用的是哪个版本的g++? - PeterT
@PeterT,已将g++版本添加到问题中。 - Maxim Kumpan
2
这是问题。 据我所知,mingw目前不支持在Windows上使用std :: thread。 最近它肯定没有支持过。 当前版本的Visual C ++在Windows上支持std :: thread。 - jcoder
1
请注意,这不是对问题的回答,而是关于此问题的一些评论。http://programmers.stackexchange.com/questions/195639/is-gcc-dying-without-threads-support-on-windows - jcoder
@MaximKumpan 顺便说一下,似乎有其他版本的MinGW(https://dev59.com/F2Uo5IYBdhLWcg3w3ycF)实际上具有工作的std :: thread支持,例如minigw-w64。 - PeterT
显示剩余3条评论

0

编译和链接时,请确保使用以下命令:

g++ -std=c++11 your_file.cpp -o your_program 

把LDFLAGS搞来搞去只会帮助链接,而不是编译。


没有用,将-std=c++11添加到CFLAGS中也没有帮助。 - Maxim Kumpan
对于 Linux 中的 g++(v4.9.2),我需要添加两个标志来支持 <thread> 和 std::thread 程序:-std=c++11 -pthread。 - mrflash818

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接