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

Posted

技术标签:

【中文标题】std::thread 管理:用法和最佳实践【英文标题】:std::thread management: usage and best practice 【发布时间】:2013-10-17 09:18:54 【问题描述】:

在 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

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

【问题讨论】:

std::thread 是一个类。我不明白你的问题。 @BjörnPollex,对不起,我的意思是说“没有命名类型”。尝试将线程对象存储在变量中不起作用。 它是一种类型。类是一种类型。 @juanchopanza,我想,但编译器仍然抛出非类型错误。 这不是问题,但是包含两个连续下划线 (__projectm__CHandler__) 的名称和以下划线后跟大写字母的名称保留给实现。不要使用它们。 【参考方案1】:

使用std::thread 作为一个简单的局部变量的问题之一是它不是异常安全的。我承认我自己在演示小的 HelloWorlds 时经常犯这个错误。

不过,最好能准确地知道你要做什么,所以这里有一个关于使用std::thread 的异常安全方面的更详细说明:

#include <iostream>
#include <thread>

void f() 
void g() throw 1;

int
main()

    try
    
        std::thread t1f;
        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()。现在可以在我的示例代码中简单地将CHandler 替换为std::thread

int
main()

    try
    
        CHandler t1f;
        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.

【讨论】:

【参考方案2】:

建议不要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 作为类的last 属性 会起作用...如果你能确保没有人从这个类派生。

因此,在我看来,现在建议只使用std::thread 作为局部变量。请注意,如果您使用async 设施,您甚至不必自己管理std::thread

【讨论】:

【参考方案3】:

Bjarne Stroustrup 在他的C++11 FAQ 中显示some examples of using std::thread。最简单的示例如下所示:

#include<thread>

void f();

struct F 
    void operator()();
;

int main()

    std::thread t1f;  // f() executes in separate thread
    std::thread t2F();    // F()() executes in separate thread

一般来说,std::thread 不打算继承自。在构造函数中传递一个异步执行的函数。

如果您的编译器不支持std::thread,您可以改用Boost.Thread。它是fairly compatible,因此一旦您的编译器支持它,您就可以将其替换为std::thread

【讨论】:

这看起来不错,但声明 std::thread _thread; 会返回 error: 'thread' in namespace 'std' does not name a type 这可能意味着你有一些包含搞砸了。如果没有完整的源代码,就不可能诊断出错误。您是否尝试过编译我发布的示例?如果它有效,那么你知道它不是你的编译器。您可能应该阅读一下 C++ 中包含的工作原理,因为它们与 Java 中的包导入完全不同。 好问题。我将尝试编译该示例,并查看线程本身是否有效。 不,它不起作用。 g++ -std=c++0x -o test test.cpp \\ test.cpp: In function 'int main()': \\ test.cpp:11:5: error: 'thread' is not a member of 'std' 没有什么比临时解决方案更持久的了。但我不认为 Boost 作为传统的线程解决方案会成为问题。我会试一试。感谢您的帮助。【参考方案4】:

首先,您使用的是什么编译器和编译器版本? std::thread 是相当新的,直到最近才在其中一些中实现。那可能是你的问题。

第二个是你

#include <thread> 

第三(这不是你的直接问题)这不是如何在 c++ 中使用线程。你不继承它,你创建一个实例传递你希望它运行的函数。

std::thread mythread = std::thread(my_func);

(虽然您可以传递的不仅仅是一个简单的函数)

【讨论】:

我通过自定义 makefile 使用 mingw 包中的 g++。 LDFLAGS=-std=c++0x。包含线程没有帮助。简单地声明 std::thread mythread 仍然会引发错误。 @MaximKumpan 你用的是哪个版本的g++? @PeterT,向问题添加了 g++ 版本。 这就是问题所在。据我所知,mingw 目前不支持 Windows 上的 std::thread 。最近肯定没有。 windows上当前版本的visual c++可以。 不回答这个问题,但这里有一些 cmet programmers.stackexchange.com/questions/195639/…【参考方案5】:

确保在编译和链接时使用:

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

弄乱 LDFLAGS 只会帮助链接,而不是编译。

【讨论】:

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

以上是关于std::thread 管理:用法和最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

52合1Node.js 最佳实践大合集

基于WPS的Word最佳实践系列(利用表格控制排版)

MVC 验证 - 使用服务层保持 DRY - 最佳实践是啥?

最佳管理实践MSP

Git 分支管理最佳实践

Firestore 用户管理最佳实践?