C++-线程的join和detach

1,146 阅读3分钟

「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

线程管理基础

启动线程

每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原始线程(以main()为入口函数的线程)同时运行。 使用C++线程库启动线程,可以归结为构造 std::thread 对象,其构造函数传入的参数是可调用对象。

void do_some_work();
std::thread my_thread(do_some_work);

C++'s most vexing parse

需要注意一个关于C++语法解析的问题:“C++’s most vexing parse”

class background_task
{
public:
 void operator()() const
 {
 do_something();
 do_something_else();
 }
};
background_task f;
std::thread my_thread(f);

这里我们构造了一个函数对象f,并传入thread的构造函数中。如果我们直接这么写

std::thread my_thread(background_task());

我们本意是希望使用background_task的构造函数返回一个background对象,并直接用这个临时对象构造一个thread对象。但是在C++中,上面的表达式会被解析为:声明了一个my_thread函数,返回值为thread对象,参数为一个函数指针,这个函数指针指向的函数没有参数,且其返回值background_task对象。 为了避免这种歧义发生,除了提前声明一个函数对象之外,还可以:

  1. 新增一对括号
std::thread my_thread((background_task()));
  1. 使用初始化语法
std::thread my_thread{background_task()};

join或detach

thread对象构造完成(线程开始执行)之后,对象析构之前,我们必须选择是等待它(join)或者让它在后台运行(detach),如果你在thread对象析构前没有这么做,那么线程将会终止,因为thread的析构函数中调用了std::terminate()

  • detach使得即使thread对象析构,线程也能继续运行,但是注意,我们要确保线程结束之前它所访问的数据都是有效的。
  • 调用thread对象的join方法,函数将等待该线程完成,然后继续执行后续语句。join将清理线程相关的存储空间

thread对象只能join或detach一次,调用过join或detach的对象再调用joinable将返回false

在发生异常的情况下join

在使用detach的时候,我们通常在构造完thread对象就立即调用detach了;而join的位置则会选在thread对象析构之前的某个位置,如果在join之前发生了异常,函数将终止,join不会被调用。为了避免这种情况发生,我们可以加上try-catch语句,并且通常来说,我们希望即使发生异常也调用join方法等待。

struct func; 
void f()
{
    int some_local_state=0;
    func my_func(some_local_state);
    std::thread t(my_func);
    try
    {
        do_something_in_current_thread();
    }
    catch(...)
    {
        t.join();
        throw;
    }
    t.join();
}

这种写法过于冗长,我们的目标本质上是希望线程完成之后函数再退出,有一种简洁的方式可以达到这个目的:资源获取就是初始化(RAII,Resource Acquisition Is Initialization)

class thread_guard
{
    std::thread &t;
public:
    explicit thread_guard(std::thread &t_) : t(t_){}
    ~thread_guard()
    {
        if (t.joinable())
        {
            t.join();
        }
    }
    thread_guard(thread_guard const &) = delete;
    thread_guard &operator=(thread_guard const &) = delete;
};
struct func;
void f()
{
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread t(my_func);
    thread_guard g(t);
    do_something_in_current_thread();
}

当f函数执行结束时,局部变量析构的顺序与构造顺序相反,即先析构g,再析构t,在thread_gurad的析构函数中调用了join函数,即使do_something_in_current_thread发生异常,join函数也会被调用(实际并不是??)。因为线程只能被join一次,因此要先判断是否joinable。 这里删除了默认的拷贝构造函数和拷贝赋值函数,这是因为拷贝后的对象的生命周期可能会超出thread对象的作用域(例如函数返回一个thread_guard对象)

detach

一旦thread对象调用了detach,线程与thread对象将不再有关联,我们也没有直接的方式与线程通信,也不再能join或detach该线程,此时线程的所有权属于C++运行时库,它保证在线程退出时相关资源被回收。分离的线程通常称为守护进程,它们通常在程序的整个生命周期运行,做一些监控、清理工作。同样的thread对象只能被detach一次