前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >c++ thread探坑

c++ thread探坑

原创
作者头像
腹黑熊
修改2022-04-21 11:02:12
1.2K0
修改2022-04-21 11:02:12
举报
文章被收录于专栏:大熊爱学习

简单记录下自己学习和使用c++ thread过程中探的坑和知识点。有错误的地方欢迎大佬指正。


joinable()为true的thread变量被销毁或被赋值会导致程序异常结束

cppreference.com中原文如下:

std::terminate() is called by the C++ runtime if a joinable std::thread is destroyed or assigned to.

std::terminate()会直接使程序终止。所以在joinable()为true的thread变量离开作用域之前,可以通过调用join()或detach()来使joinable()为false,也可以通过移动来转移所有权。

在以下四种情况下,thread变量的joinable()为false:

  • it was default-constructed (调用默认构造函数创建的thread变量)
  • it was moved from (通过移动赋值或移动构造转移线程所有权)
  • join() has been called (等待线程执行结束并主动清理线程资源)
  • detach() has been called (分离线程使其成为后台守护线程,线程结束后由c++ runtime自动清理资源)

joinable()的判断标准是是否有线程与当前thread变量关联。注意:哪怕线程已经执行完,但是没有调用join()或detach(),依然joinable()为true


thread变量无法复制

thread的移动和复制构造函数声明如下:

代码语言:c++
复制
thread( thread&& other );
thread( const thread& ) = delete;

无法通过复制构造的方式创建新的线程或者尝试用两个thread变量表示同一个线程,只能进行所有权转移。


尽量不要让新线程访问到局部变量的引用或指针

在新线程调用detach()的情况下,创建线程的函数可能会在线程执行完之前退出,局部变量所在的占空间被释放,并可能被下一个函数使用,此时程序的行为是不确定的。

只有在明确要新线程修改局部变量,并且通过join()等手段确保线程会在函数完成前结束。

补充:新线程访问局部变量指针很好理解,只需要把指针传入构造函数的参数中即可。访问局部变量的引用就能通过传参了,这种情况可以是传入可调用的对象,而对象的成员中存在对局部变量的引用。

代码语言:c++
复制
// 参考effective modern c++
struct func{
    int& i;
    func(int& i_): i(i_) {}
    void operator()() {
        // do something to i
    } 
}

void f() {
    int a = 0;
    func my_func(a);
    thread t(my_func);
    t.detach();
}

参数的传递步骤

在传递参数创建thread变量时,会首先将函数(或可调用对象)和参数通过复制或移动的方式(取决于传入的是左值还是右值)创建对应的副本,这个过程是在本地线程完成。之后对函数(或可调用对象)副本和参数副本以右值引用的参数形式在堆中创建副本的副本。最后在新线程中以最终函数(或可调用对象)的副本调用最终参数的副本。

验证代码:

代码语言:c++
复制
struct T{
    T(){};

    T(const T& t) {
        cout<<"copy construct: "<<&t<<" "<<this<<endl;
        cout<<this_thread::get_id()<<endl;
    }

    T(T&& t) {
        cout<<"move construct: "<<&t<<" "<<this<<endl;
        cout<<this_thread::get_id()<<endl;
    }

    ~T() {
        cout<<"destruct: "<<this<<endl;
    }
};

void f(T a) {
    while(1);
}

int main ()
{
    {
        T a;
        cout<<&a<<endl;
        thread t(f, a);
        t.detach();
    }
    while(1);
}

本地测试结果:

代码语言:txt
复制
0x7ffd1a6db4ef
copy construct: 0x7ffd1a6db4ef 0x7ffd1a6db4a8
140632872105792
move construct: 0x7ffd1a6db4a8 0x559255e322c8
140632872105792
destruct: 0x7ffd1a6db4a8
destruct: 0x7ffd1a6db4ef
move construct: 0x559255e322c8 0x7fe7a4629df7
140632872101632

可以看到构造thread过程实现了一次参数的复制构造和移动构造,第一次复制构造在当前线程空间创建参数的副本,第二次移动构造应该是在堆上创建了副本的副本,所以最后离开作用域之后只有原变量和第一副本被析构。第三次的移动构造就是在新线程函数构造形参,所以新线程函数参数是以std::move()的方式传入。

注意:对不可复制类型的左值参数要使用std::move()传入,这样第一步就会调用移动构造函数,否则无法编译通过。

讲真:其实并不明白为什么第一步要复制(或移动)一份副本。


thread构造函数的返回时间

根据官方构造函数说明

The completion of the invocation of the constructor synchronizes-with (as defined in std::memory_order) the beginning of the invocation of the copy of f on the new thread of execution.

构造函数会在新线程刚开始调用函数的时候返回。


利用std::ref()在新线程中使用和修改原始参数

如前面所说,所有传入新线程函数的参数都是原始参数的副本,所以任何操作都不会对原变量产生影响。如果一定要在新线程中修改原始变量的话,则可以通过std::ref()。该函数实际返回std::reference_wrapper<T>类型变量,这是一个包装类,本质上存储了参数的引用,且该引用在复制和移动时不会改变。

我们将之前的代码略作修改,在传参时使用std::ref:

代码语言:c++
复制
thread t(f, std::ref(a));

本地输出:

代码语言:txt
复制
0x7ffd54516b47
destruct: 0x7ffd54516b47
copy construct: 0x7ffd54516b47 0x7f67d2fdce17
140083898210048
140083898210048

可以看到过程中并没有对a的复制和移动,最后传入新线程函数的是原始变量的引用。同理,也可以对thread构造函数的第一个参数使用std::ref(),这种情况下就不会复制(或移动)函数指针或可调用对象。


函数参数的隐式转换在新线程执行函数时发生

考虑下面代码:

代码语言:c++
复制
// 代码参考自c++并发实战
void f(int i,std::string const& s);
void oops(int some_param)
{
    char buffer[1024];
    sprintf(buffer, "%i",some_param);
    std::thread t(f,3,buffer);
    t.detach();
}

在用户的角度可能以为buffer会在构造函数执行前自动转换为string类型,这样在新线程就不会访问原线程的局部变量。但是实际上构造函数会将buffer指针一路复制(或移动)到新线程的存储,最后在新线程中调用f,此时才会自动构建string。但是这个时候构造线程的函数可能已经退出,局部变量无效,会导致不可预测的行为。


临时变量对象构造thread注意事项

考虑下面代码:

代码语言:c++
复制
struct T {
    void operator()() {
    }
}

thread my_thread(T());

这种通过临时变量构造thread的方式会被c++编译器解析为函数声明,函数名my_thread,该函数返回一个thread对象,参数是一个函数指针,指向没有参数并返回T对象的函数。这种情况建议使用统一初始化。

代码语言:c++
复制
thread my_thread{T()};

全文参考:c++并发实战,effective modern c++

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • joinable()为true的thread变量被销毁或被赋值会导致程序异常结束
  • thread变量无法复制
  • 尽量不要让新线程访问到局部变量的引用或指针
  • 参数的传递步骤
  • thread构造函数的返回时间
  • 利用std::ref()在新线程中使用和修改原始参数
  • 函数参数的隐式转换在新线程执行函数时发生
  • 临时变量对象构造thread注意事项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档