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. 源码的简单剖析,有机会再深入研究

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/500024.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

常用类(日期时间)

目录 一、JDK 8之前的日期时间API1.1、System类中获取时间戳的方法1.2、Java中两个Date类的使用1.3、SimpleDateFormat的使用1.4、Calendar日历类的使用 二、JDK8中日期时间API的介绍2.1、LocalDate、LocalTime、LocalDateTime的使用2.2、Instant类的使用2.3、DateTimeFormatte…

Abaqus模拟新能源汽车电池理论概念

在新能源汽车电池的分析过程中&#xff0c;存在众多典型问题&#xff0c;这些问题跨越了机械、热管理和电气三大关键领域。其中&#xff0c;结构仿真分析作为一种重要的技术手段&#xff0c;主要聚焦于解决机械和热管理方面的挑战&#xff0c;为电池系统的性能优化和安全性提升…

集合(未完。。。)

集合 例题引入1.java集合引入2.为什么要使用集合&#xff1f;3.List、Set、Queue和Map的区别4.ListList——ArrayList&#xff08;&#xff01;&#xff01;实用&#xff01;&#xff01;&#xff09;ArrayList常用方法 List——VectorList——LinkedList 5.Set6.MapHashMapHas…

【CTFshow 电子取证】套的签到题

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java、PHP】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收…

Spring 源码调试问题 ( List.of(“bin“, “build“, “out“); )

Spring 源码调试问题 文章目录 Spring 源码调试问题一、问题描述二、解决方案 一、问题描述 错误&#xff1a;springframework\buildSrc\src\main\java\org\springframework\build\CheckstyleConventions.java:68: 错误: 找不到符号 List<String> buildFolders List.of…

【C++】常对象

目录 常对象常对象特点常数据成员常成员函数对象的常引用 常对象 把对象定义为常对象&#xff0c;对象中的数据成员就是常变量&#xff0c;在定义时必须带实参&#xff08;或者有缺省构造函数&#xff09;作为数据成员的初值。 const Person p1(3,4);//定义了一个常对象常对象特…

202452读书笔记|《永安梦》——错过前世 般配 换取今生 奉陪 任波谲云诡 共安危 共进退

今年追的第一本剧同名小说&#xff0c;《永安梦》改编自剧《长安第一美人》。本来是冲着徐正溪去看的&#xff0c;被娜娜、孙坚、夏楠路转粉了&#xff0c;只限这个剧。名字跟我有一个字一样诶。妆造&#xff0c;姿态&#xff0c;男女主&#xff0c;男二女二配角都不错。 因为看…

DRAGIN:利用LLM的即时信息需求进行动态RAG 论文解读

论文地址:https://arxiv.org/pdf/2403.10081.pdf DRAGIN 是一种新型的检索增强生成框架,专门为大型语言模型(LLMs)设计,以满足其在文本生成过程中的实时信息需求。该框架旨在解决传统检索增强生成(RAG)方法在动态性和准确性方面的局限性,特别是在处理复杂、多步骤或长文…

如何将Maven与TestNG集成

我们已经讨论了如何在maven中执行单元测试用例&#xff0c;但那些是JUnit测试用例&#xff0c;而不是TestNG。当maven使用“mvn test”命令进入测试阶段时&#xff0c;这些用例被执行。 本文将介绍如何将Maven与TestNG集成&#xff0c;并在maven进入测试阶段时执行TestNG测试。…

leetcode刷题日记-缺失的第一个正数(困难)

题目描述 解题思路 题目的意思十分容易理解&#xff0c;但是确实思考出来这种解题的方法还是比较难的。首先能想到的点就是[1,N]这个范围&#xff0c;因为只有N个数字&#xff0c;最小的数字只能在这个区间和N1两种可能。但是有时间复杂度的限制&#xff0c;我们该怎么找。我们…

【STM32F103】1-WireDS18B20(含ESP8266代码)

1-Wire 单总线 1-Wire是一种串行通信总线协议&#xff0c;由美国芯片制造商Dallas Semiconductor&#xff08;现为Maxim Integrated&#xff09;开发。这种协议主要用于连接和通信各种设备&#xff0c;并在多个领域得到了广泛应用&#xff0c;如温度传感器、电池管理、智能卡等…

测试用例设计方法-场景法详解

01 定义 场景法是通过运用场景来对系统的功能点或业务流程的描述&#xff0c;从而提高测试效果的一种方法。 场景法一般包含基本流和备用流&#xff0c;从一个流程开始&#xff0c;通过描述经过的路径来确定的过程&#xff0c;经过遍历所有的基本流和备用流来完成整个场景。 …

NO12 蓝桥杯单片机之DS1302的使用

1 DS1302是什么 DS1302由两块存储器组成&#xff0c;一个是日历时钟寄存器还有一个是31位的静态RAM存储器。 而在蓝桥杯中常考的就是日历时钟寄存器&#xff0c;故这里只介绍日历时钟寄存器。简单来说&#xff0c;其就是一个“电子表”&#xff0c;他会自动的实时记录时间&am…

简易挛生分拣系统设计

1 工效组合展示 2 方案规划设计 3 数字挛生建模 基础建模、动画设计、模型导出 4 软件体系架构 5 Web交互设计 5.1 页面架构 5.2 初始构造 5.3 模型运用 5.4 WS通信 5.5 运行展现 6 服务支撑编码 6.1 整体调度 6.2 WS服务 6.3 C/S通信 7 系统级调试完善

了解一下npm i的流程与原理

流程 执行npm install&#xff0c;先判断有无lock文件。 1、没有lock文件。会先根据依赖构建出扁平的依赖关系决定下哪些包。新版本的依赖关系是扁平化的&#xff0c;老版本是树结构&#xff0c;可能会出现依赖重复安装的问题&#xff0c;老版本示意图如下&#xff1a; 作为前…

【探索Linux】—— 强大的命令行工具 P.31(守护进程)

阅读导航 引言一、守护进程简介1. 概念2. 特点 二、用C创建守护进程⭕代码✅主要步骤 温馨提示 引言 当谈到计算机系统中运行的特殊进程时&#xff0c;守护进程&#xff08;daemon&#xff09;无疑是一个备受关注的话题。作为在后台默默运行并提供各种服务的进程&#xff0c;守…

【机器人】UIUC、北大、亚马逊提出基于动作的场景图,让机器人理解和操控未知环境

论文的主要内容是通过机器人操作的交互式探索&#xff0c;构建一个基于动作的场景图&#xff08;Action-Conditioned Scene Graph&#xff0c;简称ACSG&#xff09;来帮助机器人更好地理解和操作未知环境。 全文核心&#xff1a;在未知环境中如何让机器人自主探索并完成复杂任务…

乐维更改IP地址

1.1 系统IP调整 vim /etc/sysconfig/network-scripts/ifcfg-ens1921.2 Web相关服务IP变更 1.2.1 编辑/itops/nginx/html/lwjkapp/.env文件,更改ZABBIXSERVER、ZABBIXRPCURL、DB_HOST中的IP 1.2.2 进入/itops/nginx/html/lwjk_app/目录下,执行php bin/manager process-conso…

记录C++中,vector的迭代器在push_back以后扩容导致迭代器失效的问题

前言 vector是我们用到最多的数据结构&#xff0c;其底层数据结构是单端动态数组&#xff0c;由于数组的特点&#xff0c;vector也具有以下特性&#xff1a; ①O(1)时间的快速访问&#xff1b; ②顺序存储&#xff0c;所以插入到非尾结点位置所需时间复杂度为O(n)&#xff0c;删…

JDK21|史诗级的更新,虚拟线程

作者:鱼仔 博客首页: https://codeease.top 公众号:Java鱼仔 前言 要想看官方对于JDK21的更新说明&#xff0c;可以直接跳转到下面这个官方网站中 官网地址为&#xff1a;https://openjdk.org/projects/jdk/21/ JDK21是最新的LTS版本&#xff0c;里面添加了不少新的特性&…