C++线程:thread的基本使用方式


基本使用样例

这里使用一个基本的使用模版来说明thread的使用方式。

class background_task{
public:
    void operator()(){
        cout << "call background task in operator ()" << endl;
    }
};

void do_on_background(){
    cout << "call do_on_background" << endl;
}

int main(){
    // 1.
    thread t1(do_on_background);
    t1.join();

    // 2.
    background_task bt;
    thread t2(bt);
    t2.join();

    return 0;
}
/** output
call do_on_background
call background task in operator ()
*/

clang编译直接运行就行,linux下使用g++编译需要指定 -lpthread

上述代码的输出是确定的,不会出现不同的输出。基本的套路如下:

  1. 构建thread对象;
  2. 定义一个函数,作为thread构造函数的参数传入,可以是普通的对象或者是函数类的对象;
  3. 调用join函数,让主线程等子线程执行完再继续执行,保证了执行顺序;

在这个过程中,do_on_background等函数会被复制到线程的存储空间中,这些函数的执行和调用都在线程的内存空间中进行。

thread对象构造时的问题

在构造thread过程中会存在一个问题,对于普通函数do_on_background不存在,但是对于函数对象来说,会出现most vexing parse,具体来说是这样的:

我们以为下述代码的含义是:构建一个background_task的对象,并用这个临时的函数对象初始化thread对象:

thread t2(background_task());

但是,C++编译器是这么理解的:

声明了一个函数t2,返回thread对象,t2函数的参数是一个函数指针,该函数指针本质为:

background_task(*b)()

即返回值为background_task对象,且无参数的函数指针。使用这个写法来声明thread对象时,会有如下的错误出现:

错误如上

直接告诉我们,这种写法被认为是函数声明,还告诉我们一种解决方法:

  • 加个()

    thread t2((background_task()));
    
  • 还可以使用list initialization

    thread t2{background_task()};
    
  • 使用lambda表达式也是可以的;

    thread t2([]() { cout << "call lambda expression" << endl;});
    

thread对象析构时的问题

当构造完成thread对象后,必须调用join或者detach来执行,否则会报错,其原因在于创建的线程有两个状态,如果thread对象析构时,为nonjoinable时,则会直接终止,如文章所述。

如果没有使用join或者detach时,当main程序结束之后,线程对象被析构之前,如果发现线程对象是joinable时,就会直接调用std::terminate结束线程的运行,如下:

std::terminate结束线程运行

When a thread object goes out of scope and it is in joinable state, the program is terminated.

join还是detach?

两者之间有什么区别吗?

  • join

    Blocks the current thread until the thread identified by *this finishes its execution.

    在上述样例代码中,join表示main线程等待t1和t2等线程的结束,才会继续执行。

    该函数调用后,一定能保证线程的输出是确定的,不一定,只能保证创建子线程的线程与父线程之间有等待关系,多个兄弟线程之间不保证,如上代码改为下述代码就不保证输出是确定的。

    thread t1(do_on_background);
    background_task bt;
    thread t2(bt);
    t1.join();
    t2.join();
    

    对于join的调用,一个线程只能使用一次,可以通过joinable来判断线程是否允许join,奇怪的是这个函数也可以用来判断是否允许detach

    在main中调用join,其实另外一个作用是,清理线程创建使用的资源,避免资源泄露。

  • detach

    Separates the thread of execution from the thread object, allowing execution to continue independently. Any allocated resources will be freed once the thread exits.

    该函数表示main对应的线程不管t1h和t2等线程的执行,它们可以独立地执行,因此也不会等待它们,这样最终输出的结果也是不确定的。

    当线程执行结束后,其获得资源也会被释放。当main对应的线程结束后,detach的线程也不一定执行完成。当调用detach之后,在main函数中声明的线程对象与实际线程的执行没有关系,也无法进行管理了。

对象lifetime的影响

在单线程程序中,对于变量的访问,受到了其生命周期的影响,例如,当block scope结束后,其中的变量会被销毁,就不能进行访问了。但是,这种情况也很简单,因为只要保证单线程访问时,变量没有被销毁即可,这种条件,对于编程人员,可能很明显地发现来避免。

但是在多线程程序中,需要保证线程在访问变量时,变量仍然存在,此时,编程人员很难去发现,保证变量与线程的生命周期重叠。对于线程的启动,我们当前有join和detach来介入线程的实际运行方式。

  • join

    void do_on_background(int a){
        cout << a << ": call do_on_background" << endl;
    }
    
    int main(){
        int a = 0;
        thread t(do_on_background, a);
        t.join();
        return 0;
    }
    /**
    output: 0: call do_on_background
    */
    

    这里的输出一定是确定的,因为t.join()保证在main线程停止,等待t线程结束运行,此时变量a一直存在,不会销毁。

  • detach

    void do_on_background(int a){
        cout << a << ": call do_on_background" << endl;
    }
    
    int main(){
        int a = 0;
        thread t(do_on_background, ref(a));
        t.detach();
        return 0;
    }
    

    在上述代码中线程t中可就会访问不到变量a,因为当main线程结束,a被销毁,其引用也就无意义了。但是这种问题并不容易发现。

    针对这种问题,一般有两种解决方法:

    • 使用join等待,确保局部变量在线程执行完才销毁;
    • 将传递的数据进行复制,而不是引用,这样数据就在main和t线程中存在两份,分别对应个各自线程的生命周期。

    在线程之间,尤其是指针和引用等数据要注意使用和共享。

资源泄露:join的位置在哪?

在前面,我们已经发现,join的位置不同,会导致输出的结果变得确定、或者不确定。另外join还会回收资源,如果join未执行,则会导致资源泄露,常见的导致join未执行的行为包括:

  • 提前return;
  • 原始线程出现异常被抛出;

如下:

thread t(do_on_background, 1);
throw invalid_argument("error");
t.join();

上述代码不会输出“call do_on_background”,因为有异常抛出,main非正常退出。当抛出异常后,程序直接调用std::terminate,不会执行到t.join,因此线程对象t的资源没有被释放。

如果在捕捉到相关的异常后,也可以调用join,这时也能保证线程的资源被回收,如下:

thread t(do_on_background, 1);
try { 
    throw invalid_argument("error");
} 
catch(const exception &e) {
    t.join();
}
t.join();

输出如下:

线程可以正常执行不会终止

还可以使用RAII来解决这种问题:

class thread_guard
{
private:
    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;
};

void do_on_background(int a)
{
    cout << a << ": call do_on_background" << endl;
}

int main()
{
    int a = 0;
    thread t(do_on_background, a);
    thread_guard g(t);
    cout << "main over" << endl;
}

上述使用RAII来管理线程资源,当main函数退出时,对象g会被析构,然后就会调用join,使得线程对象t的资源被正确释放。但是,注意上述代码的输出是不确定的,这也是join位置造成的影响。

参考资料

  1. C++ Concurrency in action - 2.1 - Basic thread management

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录