C++多线程:thread构造源码剖析与detach大坑(三)

1、thread源码浅剖析

基于Ubuntu18.04版本64位操作系统下进行分析thread源码分析,与Window或者其他版本可能有出入。

1.1、thread线程id的源头
typedef pthread_t __gthread_t;
typedef __gthread_t			native_handle_type;

    /// thread::id
class id
{
  native_handle_type	_M_thread;
    explicit
      id(native_handle_type __id) : _M_thread(__id) { }
    .....
}

return thread::id(__gthread_self());

__gthread_self (void)
{
  return __gthrw_(pthread_self) ();
    pthread_self  这个东西并不陌生,Linux系统编程里学过,获取当前线程id号。
}
  • 从上往下看,首先看typedef pthread_t __gthread_t;这里可以看到C++的thread本质也是基于Linux系统下的pthread进行多线程的,不过一直在typedef换名字。

  • id类中就一个重要的东西,那就是native_handle_type _M_thread; 这个东西的本质就是pthread_t 表示线程id号。源码中有如下一行代码

    可以清楚的知道这就是线程id。

  • 另外还有其他很多的东西一些重载和杂七杂八的东西,这里略过,总之:thread多线程是通过pthread库的实现的。

1.2、thread的构造函数分析
private:
    id	_M_id;				// 上面的内部类
  public:
    thread() noexcept = default;
    thread(thread&) = delete;
    thread(const thread&) = delete;
    thread(const thread&&) = delete;

    thread(thread&& __t) noexcept
    { swap(__t); }



    template<typename _Callable, typename... _Args>
    explicit  thread(_Callable&& __f, _Args&&... __args) {
		#ifdef GTHR_ACTIVE_PROXY
		// Create a reference to pthread_create, not just the gthr weak symbol.
        auto __depend = reinterpret_cast<void(*)()>(&pthread_create);
		#else
            auto __depend = nullptr;
        #endif
        _M_start_thread(_S_make_state( __make_invoker(std::forward<_Callable>(__f), std::forward<_Args>(__args)...)),  __depend);
      }
  • 这里提供了很多构造函数,最简单的构造是空构造、拷贝构造、以及引用和万能引用的拷贝构造但是这些都不常用,最常用的三下面的有参构造。

  • 有参构造中提供_Callable回调函数和万能引用可变长的参数列表。

    • 第一点:多线程的创建调用的是pthread_create方法,传入的Callable函数最后会给到pthread_create中的参3,参数会给到pthread_create中的参4。

      int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
      返回值: 成功0, 失败-1
      参数:
          1). pthread_t *tid: 传出参数,获取线程的id
          2). pthread_attr_t *attr: 可以设置线程的优先级等...
          3). void *(*start_routine)(void*):线程的回调函数(线程需要执行的逻辑内容)
          4). void *arg: 参数3回调函数的参数,没有的话可以传NULL
      
    • 第二点: std::forward方法的作用就是实现完美转发,将参数按照原始模板的类型转发给其他对象,即保持所有对象的类型,决定将参数以左值引用还是右值引用的方式进行转发。

    • 第三点:__make_invoker函数的调用

      template<typename... _Tp>
            using __decayed_tuple = tuple<typename std::decay<_Tp>::type...>;
      
      template<typename _Callable, typename... _Args>
            static _Invoker<__decayed_tuple<_Callable, _Args...>>
            __make_invoker(_Callable&& __callable, _Args&&... __args)
            {
      	return { __decayed_tuple<_Callable, _Args...>{
      	    std::forward<_Callable>(__callable), std::forward<_Args>(__args)...
      	} };
        }
      
      • 这里主要就是这些代码,也不必逐行区分析区看待,这里的大致操作就是将回调函数和传入的参数一起进行decay衰退
      • decay衰退的解释: 假设T 是某种类型,当T是引用类型,decay<T>::type返回T引用的元素类型;当T是非引用类型,decay<T>::type返回T的类型。
      • 所有的东西decay衰退完毕后组成一个tuple元组
    • 第四点:_S_make_state函数进行智能指针的创建,最后就是_M_start_thread函数的执行。

      using _State_ptr = unique_ptr<_State>;
      void _M_start_thread(_State_ptr, void (*)());
      
      template<typename _Callable>
            static _State_ptr
            _S_make_state(_Callable&& __f)
            {
      	using _Impl = _State_impl<_Callable>;
      	return _State_ptr{new _Impl{std::forward<_Callable>(__f)}};
            }
      
  • 整个过程中需要知道线程是通过pthread_create创建出来的和decay衰退,但是Ubuntu和Windows下的测试并不太一样。

  • 其实核心要明白decay衰退,这里会接触所有的引用、const、volatile。

  • Ubuntu下同一份代码的会发现拷贝函数比Windows下多一次,也不知道具体发生在哪一步,可能还得进一步的分析。

2、detach与拷贝的问题
2.1、临时对象作为线程参数
#include <iostream>
#include <thread>

void myprintf(const int &i, char *buf)
{
    std::cout << i << std::endl;
    std::cout << buf << std::endl;
}

int main() {
    int i = 5;
    int& ref_i = i;
    char *mybuf = "this is a test!";
    std::thread mythread(myprintf, ref_i, mybuf);
    mythread.detach();

    std::cout << "Hello, World!" << std::endl;
    return 0;
}

在这里插入图片描述

  • 可以发现一般的引用类型会被decay退化成普通类型进行重新拷贝构造,因此基本数据类型传入不传入引用都不会造成问题,但还是推荐传值不传引用!
  • 而指针类型并没有退化,还是用原来的地址都指向着main栈帧中的同一块地址,因此当使用detach脱离时有可能造成非法访问一块不存在的空间。
2.2、指针类型解决思路

将char *类型的字符串隐式转化成一个string类型的字符串,其中这里肯定会会调用string的构造函数对字符串进行重新构造,因此地址不会相同。

#include <iostream>
#include <thread>
#include <string>

void myprintf1(const int &i, const std::string &buf)
{
    std::cout << i << std::endl;
    std::cout << buf << std::endl;
}
int main() {
    int i = 5;
    int& ref_i = i;
    char *mybuf = "this is a test!";
    std::thread mythread(myprintf1, ref_i, mybuf);
    mythread.detach();
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

在这里插入图片描述

2.3、隐式转化由谁实现

上述2.2的代码看起来没有问题,实则隐式转化是存在问题的,当char * --> string需要花费很长时间时。可能主线程执行完毕需要释放这一块的空间,而拷贝还没有完成,因此这个转换交给谁来是有一个严格的说法的。

 // 交给子线程构造转换
 std::thread mythread(myprintf1, ref_i, mybuf);
 
 // 交给主线程构造转换出一个string
  std::thread mythread(myprintf1, ref_i, string(mybuf));

为了方便测试打印效果,我们采用join等待的方式展示出来,实际上这些问题都是detach方法带来的问题,与join没有半毛钱关系。

2.3.1、隐式构造(子线程构造)
class A{
public:
    int m_i;
    A(int a): m_i(a) {
        std::cout << "[A::A(int a)构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    A(const A &a): m_i(a.m_i){
        std::cout << "[A::A(const A &a)拷贝构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    ~A(){
        std::cout << "[A::~A()析构函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
};
void myprintf2(const A &a)
{
    std::cout << "子线程&a = " << &a << ", thread_id = " << std::this_thread::get_id() << std::endl;
}
int main() {
    std::cout << "主线程 thread_id = " << std::this_thread::get_id() << std::endl;
    int a = 10;
    std::thread mythread(myprintf2, a);
    mythread.join();
    return 0;
}

请添加图片描述

  • 可以看到首先主线程一768结尾,子线程是752结尾。

  • 这种隐式构造我们有理由怀疑:当主线程768执行完毕,释放完空间时,子线程752才开始拷贝这个对象,恰巧这个对象的空间地址已经被释放,那么子线程可能越界访问一块不存在的地址空间,从而导致错误。

  • 总结:子线程在自己从外面拷贝东西到自己的空间,而需要拷贝的东西可能被提前释放。

2.3.2、临时变量(主线程拷贝)
class A{
public:
    int m_i;
    A(int a): m_i(a) {
        std::cout << "[A::A(int a)构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    A(const A &a): m_i(a.m_i){
        std::cout << "[A::A(const A &a)拷贝构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    ~A(){
        std::cout << "[A::~A()析构函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
};
void myprintf2(const A &a)
{
    std::cout << "子线程&a = " << &a << ", thread_id = " << std::this_thread::get_id() << std::endl;
}
int main() {
    std::cout << "主线程 thread_id = " << std::this_thread::get_id() << std::endl;
    int a = 10;
    std::thread mythread(myprintf2, A(a));
    mythread.join();
    return 0;
}

在这里插入图片描述

  • 可以看到主线程728,子线程712
  • 重点关注子线程持有的对象(地址07288的对象),它是由728线程拷贝好了塞到712线程的私人空间中的,这样就能保证主线程728在向下执行之前一定会把子线程712需要的东西已经交付完毕!
  • 对比上面子线程自己拷贝存在很大的区别,一个是自己伸手去拿,一个是别人塞给你!
  • 但是这里拷贝构造两次一直没弄清楚,同一份代码Window下是只有一次拷贝构造的,Ubuntu下为什么是两次不清楚,可能还需要剖析源码!
3、join与引用

然而并不是所有的方法都需要detach让子线程脱离主线程,有一些情况是需要主子线程协同运行,对于这种情况我们可以使用std::ref()传入,并且使用Join方法即可,而如果使用detach方法那么情况可能就不一样了。

class A{
public:
    int m_i;
    A(int a): m_i(a) {
        std::cout << "[A::A(int a)构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    A(const A &a): m_i(a.m_i){
        std::cout << "[A::A(const A &a)拷贝构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    ~A(){
        std::cout << "[A::~A()析构函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
};
void myprintf3(A &a)
{
    a.m_i = 200;
    std::cout << "子线程&a = " << &a << ", thread_id = " << std::this_thread::get_id() << std::endl;
}
int main() {
    std::cout << "主线程 thread_id = " << std::this_thread::get_id() << std::endl;
    A a(10);
    std::cout << "a.m_i = " << a.m_i << std::endl;
    std::thread mythread(myprintf3, std::ref(a));
    mythread.join();
    std::cout << "a.m_i = " << a.m_i << std::endl;
    return 0;
}

在这里插入图片描述

4、成员函数作为线程的回调函数
class A{
public:
    int m_i;
    A(int a): m_i(a) {
        std::cout << "[A::A(int a)构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    A(const A &a): m_i(a.m_i){
        std::cout << "[A::A(const A &a)拷贝构造函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id()<<std::endl;
    }
    ~A(){
        std::cout << "[A::~A()析构函数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id() <<std::endl;
    }

    void thread_work(int num){
        std::cout << "[A::thread_work数执行]" << ", this address = " << this << ", thread_id = " << std::this_thread::get_id() <<std::endl;
    }
};
void test4()
{
    std::cout << "主线程 thread_id = " << std::this_thread::get_id() << std::endl;
    A a(10);
    std::cout << "a.m_i = " << a.m_i << std::endl;
    std::thread mythread(myprintf3, std::ref(a));
    mythread.join();
    std::cout << "a.m_i = " << a.m_i << std::endl;
}
int main() {
    A a(10);
    std::thread mythread(&A::thread_work, a, 15);				// 15对应int num
    mythread.join();
    return 0;
}

在这里插入图片描述

5、总结
  1. 当使用detach方法脱离主线程时需要注意拷贝的问题,绝对不能使用隐式转换。
  2. std::ref函数传入引用、std::decay类型衰退、自定义成员函数当线程的回调函数
  3. 源码的简单剖析,有机会再深入研究
  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
std::thread::detach()是C++11中引入的线程标准库中的一个成员函数。它用于将创建的线程与主线程解绑,使得线程在后台运行,不再与主线程进行同步。具体来说,在调用std::thread对象的detach()函数之后,该线程将成为"detached"状态,即与主线程解绑。 这个函数在Linux中的应用也是如此,它允许我们在创建线程后,将其与主线程分离,使得主线程可以继续执行其他任务,而不必等待该线程的结束。这对于一些需要并发执行的任务非常有用。 下面是一个示例代码,展示了std::thread::detach()函数的使用: ``` #include <iostream> #include <thread> void thread_function() { std::cout << "Inside Thread :: ID = " << std::this_thread::get_id() << std::endl; } int main() { std::thread threadObj(thread_function); if (threadObj.joinable()) { threadObj.detach(); } if (threadObj.joinable()) { std::cout << "Detachable Thread" << std::endl; } else { std::cout << "Non-Detachable Thread" << std::endl; } return 0; } ``` 在这个示例中,我们创建了一个线程对象threadObj,并传入一个函数thread_function作为线程的入口点。然后我们调用threadObj.detach()将该线程与主线程解绑。最后,通过判断threadObj.joinable()的返回值,我们可以确定该线程是否是可分离的。 总结来说,std::thread::detach()函数用于将创建的线程与主线程解绑,使得线程在后台运行,不再与主线程进行同步。它在C++11标准中引入,可以方便地编写与平台无关的多线程程序。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [linux C++ std::thread::detach()函数(线程分离)](https://blog.csdn.net/Dontla/article/details/125131895)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [c++11中关于std::thread的join的详解](https://download.csdn.net/download/weixin_38751537/13990981)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值