《C++ 并发编程实战》读书笔记: 管理线程
分类:技术
最近终于找到本有兴趣的书啦,就是这本《C++ 并发编程实战》。我会找一些自己觉得没想到、好玩、或者有自己想法的地方整理下。
顺带吐个槽:In Action 系列的封面人物真心诡异,还是 O'reilly 萌萌哒的奇怪可爱小动物看着顺心。
join()
的异常安全
std::thread
要求在析构函数被调用时,该对象必须没有与之相关联的线程,否系统分分钟自尽给你看(调用 std::terminate()
)。
一般情况下,我们主要需要注意的地方是析构前对 join()
或者 detach()
的调用。如果要分离线程,线程启动后就可以立即调用 detach()
,基本不会带来什么问题。而 join()
则没法立即调用,这样就会带来可能会被忽视的异常安全问题:线程启动后,如果某处抛出异常,可能把本应执行的 join()
跳过,导致程序自行退出。
void foo()
{
std::thread t(thread_func);
do_something(); // 如果抛出异常,就可能把下面的 join() 跳过
t.join();
}
把 do_something();
用 try
/ catch
包裹起来,并在 catch
里调 t.join()
最后把异常重新 throw
出去的方案固然可行,但对于 C++ 而言,更好的方案是 RAII:
class thread_guard
{
public:
explicit thread_guard(std::thread& t)
: thread_(t)
{
}
~thread_guard()
{
if (thread_.joinable()) { // join() 前对象必须满足 joinable
thread_.join();
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
private:
std::thread& thread_;
};
void foo()
{
std::thread t(thread_func);
thread_guard g(t);
do_something();
}
当然你也可以考虑把 std::thread
移动进去而不是传引用,书里管那种东西叫 scoped_thread
,调用时不必为 std::thread
对象单独开一个名字。
线程传参二三事
给线程函数传参是通过 std::thread
的构造函数完成的,其原理与 std::bind
类似。所有参数先在当前线程被复制 / 移动到对象内部,然后在新线程中完成对线程函数的调用。
这两步的划分会导致会被一些人说「坑」(虽然实际上是他们自己没注意)。
比如从「被复制」到「实际调用线程函数」之间可能并不连续,那么被复制的参数届时是否还有效就可能成为问题,例如:
void thread_func(std::string const& s);
void foo(int bar)
{
char buffer[1024];
sprintf(buffer, "%d", bar);
std::thread t(thread_func, buffer);
t.detach();
}
前面说过构造函数仅负责原样复制 / 移动参数,所以 char *
到 std::string
的转换是在新线程中的第二步时做的,于是例子中 t
对象构造函数暂存的其实是指向栈上的 buffer
的指针。而由于 detach()
,foo
返回后可能新线程还未正式启动。如果 thread_func
真正被调用时 foo
已经返回,那么这个指针参数就无效了。
一个解法是把代码改成 std::thread t(thread_func, std::string(buffer));
这样构造函数就会把实参 std::string
移动进去(即便在 std::string
无法移动只能复制的平行宇宙,这样做也是安全的)。
楼主你的美国游记文章怎么好像没了。。正准备参考- -!
您好,我想问下在在 std::string 无法移动只能复制的平行宇宙,为什么这样做也是可以的啊-.-
因为之前造成问题的原因是新线程用老线程中的指针初始化 std::string,而初始化时可能老线程中已经释放了那块内存,新线程中的指针就不再指向有效的内存块,变成了野指针。使用 std::string 传递的话,内存都是 std::string 管理的,消除了造成野指针的可能。