C++并发与多线程 3_线程传参数详解,detach 注意事项

Posted TianSong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++并发与多线程 3_线程传参数详解,detach 注意事项相关的知识,希望对你有一定的参考价值。

获取线程id

  • std::this_thread::get_id()
#include <iostream>
#include <thread>

using namespace std;

std::thread::id main_thread_id = std::this_thread::get_id();    // 注意这里!!!

void is_main_thread()
{
    cout << std::this_thread::get_id() << endl;                 // 注意这里 !!                            

    if (main_thread_id == std::this_thread::get_id())           // 注意这里 !!
        cout << "This is the main thread" << endl;
    else
        cout << "This is not main thread" << endl;
}

int main()
{
    is_main_thread();

    thread th(is_main_thread);
    
    th.join();

    return 0;
}

输出:

1
This is the main thread
2
This is not main thread

线程调用对象的参数传递

引用作为参数

引用作为参数时,发生参数拷贝而不是引用传递;
同时使用 std::ref() 与 thread::detach() 时,需要考虑主线程中的局部属性资源(对象)是否被子线程使用,并且主线程是否先于子线程结束。

使用 thread::detach() 主线程与子线程分离独立运行,使用 std::ref() 子线程真正引用所传递实参。
当实参在主线程中拥有局部属性,并且主线程先于子线程结束,那么主线程实参资源释放,子线程的形参引用便指向未定义的资源,到这里恭喜你,驶入了通向未定义的快车道。
C++ 本身有引用(&),为什么又引入 std::ref 呢?
  • 主要是考虑函数式编程(如 std::thread, std::bind)在使用时,是对参数直接拷贝,而不是引用;
  • std::thread, std::bind 一个函数模板,它的原理是根据已有的模板生成一个函数,但是由于std::thread, std::bind 不知道生成的函数执行的时候,传递进来的参数是否还有效。所以它选择参数传递而不是引用传递。如果引用传递,std::ref 和 std::cref 就排上用场了。
#include <iostream>
#include <thread>

using namespace std;

void thread_func(const int &i)
{
    cout << "thread_func begin" << endl;

    cout << "i = " << i << endl;
    const_cast<int &>(i) = i * 2;
    cout << "i = " << i << endl;

    for (int i=0; i<1000; ++i)
    { }
    
    // 操作 obj.i ...

    cout << "thread_func end" << endl;
}

int main()
{
    cout << "main begin" << endl;

    int i1 = 10;
    cout << i1 << endl;
    thread th1(thread_func, i1);                // 注意这里 i1 !!!
    th1.join();  
    // th1.detach();                            // 实参 i1 被拷贝,子线程可正常运行
    cout << i1 << endl;

    cout << "=================" << endl;

    int i2 = 10;
    cout << i2 << endl;
    thread th2(thread_func, std::ref(i2));      // 注意这里 std::ref(i2) !!!
    th2.join();
    // th2.detach();                            // 实参被引用,主线程结束时局部 i2 释放, 子线程引用释放资源,行为未定义!
    cout << i2 << endl;

    cout << "main end" << endl;

    return 0;
}

输出:

main begin
10
thread_func begin
i = 10
i = 20
thread_func end
10                          // 注意这里输出 !!!
=================
10
thread_func begin
i = 10
i = 20
thread_func end
20                          // 注意这里输出 !!!
main end

补充证明:注意类内拷贝构造函数打印

#include <iostream>
#include <thread>

using namespace std;

class Base {
public:
   Base(int i = 0) : m_i(i)
   {
        cout << "Base(int i) " << m_i << endl;
   }

   Base(const Base &obj) : m_i(obj.m_i)
   {
       cout << "Base(const Base &obj) " << m_i << endl;
   }

   ~Base()
   {
       cout << "~Base() " << m_i << endl;
   }

public:
   int m_i;
};

void thread_func(const Base &obj)
{
    cout << "thread_func begin" << endl;
    cout << obj.m_i << endl;
    
    for (int i=0; i<1000; ++i)
    { }
    
    // 操作 obj.m_i ...
    
    cout << "thread_func end" << endl;
}

int main()
{
    cout << "main begin" << endl;

    Base obj1{1};
    Base obj2{2};

    cout << "=================" << endl;

    thread th1(thread_func, obj1);              // 注意这里 obj1 !!!
    th1.join();
    // th1.detach();                            // 实参obj1被拷贝,子线程可正常运行
    
    cout << "=================" << endl;

    thread th2(thread_func, std::ref(obj2));    // 注意这里 std::ref(obj2) !!!
    th2.join();
    // th2.detach();                            // 实参被引用,主线程结束时局部 obj2 释放, 子线程引用释放资源,行为未定义!
    
    cout << "=================" << endl;

    cout << "main end" << endl;

    return 0;
}

输出:

main begin
Base(int i) 1
Base(int i) 2
=================           // 注意这里, 拷贝构造函数被调用!!!
Base(const Base &obj) 1     // (调用两次原因可见上一章)     
Base(const Base &obj) 1
~Base() 1
thread_func begin
1
thread_func end
~Base() 1
=================           // 注意这里, 拷贝构造函数未被调用!!!
thread_func begin
2
thread_func end
=================
main end
~Base() 2
~Base() 1

指针作为参数

指针作为形参时,形参与实参指向同一地址;
同时使用 指针 与 thread::detach() 时,需要考虑主线程中的局部属性资源(对象)是否被子线程使用,并且主线程是否先于子线程结束。

使用 thread::detach() 主线程与子线程分离独立运行,指针作为形参时,形参与实参指向同一地址。
当实参在主线程中拥有局部属性,并且主线程先于子线程结束,那么主线程实参资源释放,子线程的形参引用便指向未定义的资源,到这里恭喜你,驶入了通向未定义的快车道。
#include <iostream>
#include <thread>

using namespace std;

void thread_func(const char *pt)
{
    cout << "thread_func begin " << pt[0] << endl;

    cout << "pt addr : " << static_cast<const void *>(pt) << endl;

    for (int i=0; i<1000; ++i)
    { }
    
    // 操作 pt ...

    cout << "thread_func end" << endl;
}

int main()
{
    cout << "main begin" << endl;

    const char *pt1 = "1";                  // 局部资源
    const char *pt2 = new char(\'2\');        // 主线程管理的资源

    thread th1(thread_func, pt1);
    th1.join();
    // th1.detach();                        // 实参、形参指向同一资源,未定义快车道
    
    thread th2(thread_func, pt2);
    th2.join();
    // th2.detach();                        // 实参、形参指向同一资源,未定义快车道
        
    cout << "pt1 addr : " << static_cast<const void *>(pt1) << endl;
    cout << "pt2 addr : " << static_cast<const void *>(pt2) << endl;

    delete pt2;

    cout << "main end" << endl;

    return 0;
}

输出:【实参、形参指向同一地址】

main begin
thread_func begin 1
pt addr : 0x405043          // 注意这里 A 
thread_func end
thread_func begin 2
pt addr : 0xf617f0          // 注意这里 B
thread_func end
pt1 addr : 0x405043         // 注意这里 A\'
pt2 addr : 0xf617f0         // 注意这里 B\'
main end

发生隐式转换的对象作为参数

当使用可能发生隐式转换的对象作为参数时,应避免发生隐式类型转换,而是是在调用线程主动生成临时对象

当使用可能发生隐式转换的对象作为参数时,将在子线程执行流使用主线程中实参构造对象。
当实参在主线程中拥有局部属性,并且主线程先于子线程结束,那么主线程实参资源释放,此时隐式转换构造初值未定义,到这里恭喜你,驶入了通向未定义的快车道。
#include <iostream>
#include <thread>

using namespace std;

class Base {
public:
   Base(int i = 0) : m_i(i)
   {
        cout << "Base(int i) " << std::this_thread::get_id() << endl;
   }

   Base(const Base &obj) : m_i(obj.m_i)
   {
       cout << "Base(const Base &obj) " << std::this_thread::get_id() << endl;
   }

   ~Base()
   {
       cout << "~Base() " << std::this_thread::get_id() << endl;
   }

public:
   int m_i;
};

void thread_func(const Base &)
{ }

int main()
{
    cout << "main begin" << endl;

    cout << "main thread id " << std::this_thread::get_id() << endl;

    int i = 10;
    thread th1(thread_func, i);
    th1.join();
    // th2.detach();                        // 隐式转换发生在子线程中,此时无法保证主线程未执行结束,即无法保证主线程初始资源有效 !!!

    cout << "=================" << endl;

    thread th2(thread_func, Base(i));      // 注意这里, Base(i) 主动生成临时对象 !!! 
    th2.join();

    cout << "main end" << endl;

    return 0;
}

输出:

main begin
main thread id 1
Base(int i) 2               // 注意这里,子线程中使用主线程局部变量发生隐式转换 !!!
~Base() 2
=================
Base(int i) 1
Base(const Base &obj) 1     // 注意这里,隐式转换发生在主线程(调用线程)中,初始值此时有效 !!!
Base(const Base &obj) 1
~Base() 1
~Base() 1
~Base() 3
main end

总结

  • 如果基础数据类型作为参数,推荐使用值传递而不是引用。
  • 如果自定义数据类型作为参数,应避免发生隐式类型转换。做法是在调用线程主动生成临时对象,然后在子线程使用引用接收。
  • 尽量使用 join 而不是 detach(越简单越可靠)。

以上是关于C++并发与多线程 3_线程传参数详解,detach 注意事项的主要内容,如果未能解决你的问题,请参考以下文章

C++并发与多线程 4_创建多个线程数据共享问题分析

C++并发与多线程 2_线程启动结束,创建线程多种方法,join,detach

C++并发与多线程 10_shared_futureautomic

C++并发与多线程 9_asyncfuturepackaged_taskpromise

C++并发与多线程 11_std::atomic叙谈std::launch(std::async) 深入

C++并发与多线程 8_condition_variablewaitnotify_onenotify_all