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 管理:用法和最佳实践的主要内容,如果未能解决你的问题,请参考以下文章