TimothyQiu's Blog

keep it simple stupid

《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 无法移动只能复制的平行宇宙,这样做也是安全的)。

C++

已有 2 条评论 »

  1. wsyzdhl wsyzdhl

    楼主你的美国游记文章怎么好像没了。。正准备参考- -!

  2. wsyzdhl wsyzdhl

    楼主你的美国自驾游记文章怎么没了。。正准备参考呢- -!

添加新评论 »