【LINUX操作系统】线程操作

了解了线程的基本原理之后,我们来学习线程在C语言官方库中的写法与用法。

1. 常见pthread接口及其背后逻辑

1.1 pthread_create

与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以“pthread_”打头的
要使⽤这些函数库, 要通过引⼊头文件 <pthread.h>
链接这些线程函数库时要使⽤编译器命令的“-l pthread”选项
   pthread_create 函数并不是Linux提供的创建线程的系统调用,而是Linux中对轻量级进程已有的接口进行封装的动态库中的函数
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;void *routine(void *arg)
{//值得注意的是,线程执行的函数的返回值一定是一个void*,用于返回一个指针-》这样的目的是让所有类型的参数都能被返回//包括但不限于类对象,函数,容器等等。//而参数void*,是一种泛型编程的实现方式,这样一来可以根据传入的内容来指定完成任务。while (true){cout << "new thread" << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, run, (void *)"thread-1");while (true){cout << "main thread" << endl;sleep(1);}return 0;
}

 由实验结果可知,main和new thread的执行顺序并不固定。

         两个线程是异步打印(使用)到同一个文件资源(显示器)上,所以存在打印错乱的情况,但是的确可以做到每一个线程完成自己的任务而不会受到其他线程的干扰,这两个线程就是两个不同的执行流,而因为routine函数始终在访问同一个资源,所以此时其是不可重入函数。

pthread_create的底层其实是clone-------一个非常复杂不便于使用的函数。

由于Linux在内核代码中并没有将进程和线程完全分成两个系统管理,所以clone的flag参数就有以下两种选择,创建一个parent(进程),或者是只创建一个轻量级进程(clone_thread)

创建进程还是创建线程?一个文字游戏而已

了解:clone第一个参数,即传给该线程的函数,第二个是栈空间(那么多虚拟内存的段,为什么要单独传一个这?),最后一个是线程要执行的函数的参数。。。。。。。

所以,现在就可以进一步理解用户级线程

        

pthread_create调用的是clone,clone其实也是被封装的,底层还有如do_clone等函数。

最后,关于pthread_create的返回值:

        pthreads函数出错时不会设置全局变量errno(⽽⼤部分其他POSIX函数会这样做)。⽽是将错 误代码通过返回值返回。没有出错返回0,出错返回错误码。

         对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要⽐读取线程内的errno变量的开销更小。(strerror和errno都是之前介绍进程的时候提及的概念)

1.2 pthread_self

pthread_self用于查看任意线程的线程ID

在上一文中,我们提到以下指令,用于查看:

        ps -aL | head -1 && ps -aL | grep thread

或者

while :; do ps ajx |head -1 && ps ajx | grep test | grep -v grep;sleep 1;echo"------------------------------------------------";done

这个线程的tid值似乎与之前提到的lwp不太一样呢???

请记住这个伏笔,会在之后的内容中重点聊。

如果觉得这个数字太长,我们可以:

string ToHex(pthread_t tid)
{char buffer[64];snprintf(buffer,sizeof(buffer),"0x%lx",tid);return buffer;
}

这个tid似乎看着很像地址???????? 

多线程状态对于资源的使用

能够使用以上两个接口,我们便能创建一些多线程的demo。

下面我们来验证一下多线程的栈区是否是独立的:

        

//.....................
void *run(void *arg)
{//值得注意的是,线程执行的函数的返回值一定是一个void*,用于返回一个指针-》这样的目的是让所有类型的参数都能被返回//包括但不限于类对象,函数,容器等等。//而参数void*,是一种泛型编程的实现方式,这样一来可以根据传入的内容来指定完成任务。string name = static_cast<const char*>(arg);int val = 0;while (true){cout << "new thread,name : "<<name << " my_threadID : "<< ToHex(pthread_self())<<"my val : "<<val<<endl;val++;if(name=="thread-1") val++;sleep(1);}
}int main()
{pthread_t tid1;pthread_t tid2;pthread_create(&tid1, nullptr, run, (void *)"thread-1");pthread_create(&tid2, nullptr, run, (void *)"thread-2");
//...........

实验现象也不出所料:两个栈的val值互不影响。

两个新线程都在修改val,但是二者并没有影响到另外的val,这也验证了每个线程实际上也有自己的栈。

再尝试让两个线程都修改同一个全局变量:

果然,两个线程在某种意义上已经形成了“通信”。

        将val变量作为了全局变量供两个线程进行访问,此时两个线程均看得到这个变量,所以这个变量就是两个线程的共享资源,与前面显示器打印问题一样,因为共用并且没加保护,导致一个线程的修改会影响到另一个线程,这个效果同样适用于静态变量

两个线程谁先执行不一定,并且不同的线程会瓜分时间片

其实,独立的线程不代表是私有的线程,强行使用地址去访问某一个线程栈的内容依然能访问到。 

结果:

                      

但是这样的做法是非常不推荐的,是错误的。也在一定的程度上的说明,只要拿到虚拟地址就能访问,一切的“不允许”都是被规则规定的。

 

线程局部存储 

如果希望一个变量被每一个线程都单独创建副本,则使用__thread来修饰该变量。该关键词只能用于修饰内置类型。

栈的特性与存储

栈是每个线程私有的内存区域,用于存储局部变量、方法参数和返回地址等。每个新线程都会创建自己的栈。栈上的数据独立于其他线程,这意味着如果一个线程在其栈中创建了一个变量,其他线程不能直接访问该变量,除非通过方法参数或共享对象显式传递。这种独立性提供了天然的线程安全,因为每个线程都有自己的栈空间,不会无意间影响其他线程的数据。

堆的特性与存储

堆是一个全局共享的内存区域,用于动态分配内存。所有线程都可以访问堆上分配的对象。如果一个对象在堆上被创建,并且其引用(内存地址)被传递给多个线程,那么这些线程就可以同时访问甚至修改该对象的内容。由于堆是共享的,所以在多线程环境下访问堆上的对象时需要特别小心,通常需要使用同步机制来避免竞态条件或数据不一致的问题。

总之,不论是堆上的数据还是栈上的数据,只要能获取到指定内容的地址,就不存在所谓的栈独立性,此刻栈和堆都是共享的,而所谓的栈独立性和堆共享性更强调所有的线程都有自己的栈,但是堆共有一个


1.3 pthread_join

        已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
       创建新的线程不会也不能复⽤刚才退出线程的地址空间。
        为了解决类似于进程中“僵尸进程”的问题,并且获得线程的任务执行情况。被主线程创建的线程一般需要被等待并且回收。

参数 :
thread: 线程 ID(希望被join掉,也就是希望被终止的线程ID)
value_ptr: 它指向⼀个指针(双重指针),是线程执行的函数的返回值的地址。
返回值:成功返回 0 ;失败返回错误码。

 比如我们现在不需要线程执行的任务的返回值:

pthread_join的第二个参数是一个输出型参数,因为线程执行的函数是一个void*类型的返回值,为了让这样的void*参数能够自动适应所有的返回值,所以必须使用一个void**的参数

 线程执行的任务想返回一个10,所以就用void*强转一下10(ret是一个void*,用于接受这个10的值,所以10必须强转成void*)。

 void*是8个字节(现在的linux机器或者其他机器几乎都是64位),此时10被当成了一个void*

                                        ​​​​​​​        

所以用int去强转会出现报错,应该采用long long去强转。

或者用一个堆指针来完成这个任务也可以,先把void*的指针变成int*,再解引用。

一个线程只要return,就退出了

一个void**类型的变量ret去记录线程执行的函数的返回值(return的值)

错误使用:

这么做虽然能正确找到这个ret值,但是是不科学的,因为a是一个栈上的变量,按理来说这个变量会销毁。更建议使用堆指针

以上代码说明说明堆空间是共享的。线程之间能拿到其他线程创建的堆资源,不过后续需要自己销毁资源

另外,直接pthread_join自己会报错:

    

既然pthread_create的回调函数支持传入任何参数,那么假设我们希望给每一个线程传一个结构体,并且打印、操作该结构体对应的数据。

一个简单的传结构体指针demo:

class ThreadData
{
public:ThreadData(const string &name, int a, int b): _name(name), _dataA(a), _dataB(b){}pair<int, int> GetInfo(){return pair<int, int>(_dataA, _dataB);}const int GetA(){return _dataA;}void Exec(){_dataA++;_dataB *= 10;cout << _name << endl;}~ThreadData() {}private:string _name;int _dataA;int _dataB;
};void *routine(void *th_data)
{ThreadData *p_data = (ThreadData *)th_data;//while (true)//{cout << "new thread,myID: " << ToHex(pthread_self()) << " A:B " <<p_data->GetInfo().first << " : " << p_data->GetInfo().second << endl;p_data->Exec();cout << "new thread,myID: " << ToHex(pthread_self()) << " A:B " << p_data->GetInfo().first << " : " << p_data->GetInfo().second << endl;sleep(1);//}return (void*)p_data;
}int main()
{pthread_t tid;ThreadData *th_p = new ThreadData("thread-1", 10, 20);int err_n1 = pthread_create(&tid,nullptr,routine,(void*)th_p);if(err_n1!=0){cerr<<"create error "<<strerror(err_n1)<<endl;return 1;}else {cout<<"create success"<<endl;}void* ret = nullptr;int err_n2 = pthread_join(tid,&ret);if (err_n2 != 0){cerr << "join error" << strerror(err_n2) << endl;}elsecout << "joined success! " << "ret num A : " << ((ThreadData*)ret)->GetA() << endl;
}

创建多个线程的demo:

之前的ThreaData就是为了在一个新的线程的时候有对应的数据和方法              

  pthread_join在进行的其实也是一种阻塞等待,这会影响主线程的工作效率。


1.4 线程分离pthread_detach

        默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则⽆法释放资源,从⽽造成系统泄漏。
        如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃动释放线程资源

join和detach是矛盾的。

        需要注意的是,在多执行流的情况下,一定要确保主执行流最后退出,防止主执行流先退出导致 分离的线程 需要的资源被释放导致错误。线程可以自主分离(在线程的执行函数中执行pthread_detach(pthread_self()); 或者被迫分离-》主线程等其他线程调用该函数。

线程程序替换

        注意,进程程序替换是不可以直接发生在线程执行流中的,因为程序替换的本质是替换掉当前进程的代码,此时线程的代码也会被替换从而导致错误,但是可以在线程中创建子进程,再在子进程中使用进程程序替换

                                     ​​​​​​​

1.5 线程退出

首先,不能直接用exit(),因为exit()会直接让整个进程都退出。

pthread_exit(),pthread_exit的参数也会被pthread_join给拿到----------->因为pthread_join本来就是拿的return的值,现在由pthread_exit完成这个“return”的任务。

pthread_cancel

pthread_cancel

:主要用于主线程去取消其他进程。(类似于父进程去取消子进程的过程)

但是不建议直接使用,因为不清楚被取消的线程的工作状态

一个被pthread_cancle取消的线程的join值是-1(ret 返回的值)

这个接口要慎用,如果在新线程未完成任务就被主线程结束,主线程可能也会结束。

并且主线程在cancle的时候可能不清楚新线程的状态,有可能出错。 

一个被pthread_cancle取消的线程的join值是-1(ret    )


2. CPP的封装

除了C语言对系统的相关线程操作进行了封装外,其他编程语言也会做同样的事情,这样可以确保语言具有可移植性,同样,C++中也有对应的线程库<thread>,具体接口见C++线程库部分,但是需要注意的是,如果是在Linux下使用C++线程库编写代码,编译该代码时依旧需要携带-lpthread,因为C++线程库本质也是封装了libpthread.so

                                错误:   

lambda表达式构造thread:

   

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【AI面试准备】Azure DevOps沙箱实验全流程详解

介绍动手实验&#xff1a;通过 Azure DevOps 沙箱环境实操&#xff0c;体验从代码提交到测试筛选的全流程。如何快速掌握&#xff0c;以及在实际工作中如何运用。 通过 Azure DevOps 沙箱环境进行动手实验&#xff0c;是快速掌握 DevOps 全流程&#xff08;从代码提交到测试筛选…

VulnHub-DC-2靶机

主机发现 sudo arp-scan -l 以sudo管理员权限扫描本地活动ip地址 Interface: eth0, type: EN10MB, MAC: 08:00:27:22:46:4f, IPv4: 192.168.252.230 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.252.6 4c:5f:70:74:3c:3b …

藏语英语中文机器翻译入门实践

&#x1f3af; 项目目标&#xff1a; 输入藏文句子&#xff0c;自动翻译成英文和中文&#xff08;或输入中文&#xff0c;翻译为英文和藏文&#xff09;。 &#x1f50d; 技术与原理简介 机器翻译&#xff08;Machine Translation, MT&#xff09;是人工智能中自然语言处理&a…

【阿里云大模型高级工程师ACP习题集】2.9 大模型应用生产实践(上篇)

练习题 【单选题】在自然语言处理的法务咨询场景中,以下哪种模型选择最为合适? A. 通用大语言模型 B. 经过数学领域微调的模型 C. 面向法律领域训练的模型 D. 视觉模型 【多选题】以下哪些属于模型非功能性需求?( ) A. 模型对不同语言的支持能力 B. 模型的响应速度要求 C.…

WPF之ProgressBar控件详解

文章目录 1. ProgressBar控件简介2. ProgressBar的基本属性和用法2.1 基本属性2.2 基本用法2.3 代码中修改进度 3. 确定与不确定模式3.1 确定模式&#xff08;Determinate&#xff09;3.2 不确定模式&#xff08;Indeterminate&#xff09; 4. 在多线程环境中更新ProgressBar4.…

IntelliJ IDEA 保姆级安装教程(附安装包)

文章目录 一、下载二、安装三、启动 一、下载 Ultimate 2021.1.1 - Windows x64 (exe) 二、安装 三、启动 首次安装启动 非首次安装启动

Performance API 性能上报

以下是关于 Performance API 性能上报的基本知识点总结: 一、性能监控核心指标体系 1. 关键性能指标(Web Vitals) 指标标准采集方式健康阈值LCP (最大内容绘制)测量加载性能PerformanceObserver≤2.5sFID (首次输入延迟)测量交互响应PerformanceObserver≤100msCLS (累积布…

C语言-指针(一)

目录 指针 内存 概念 指针变量 取地址操作符&#xff08;&&#xff09; 操作符“ * ” 指针变量的大小 注意 指针类型的意义 作用 void * 指针 const修饰指针变量 const放在*前 const放在*后 双重const修饰 指针的运算 1.指针 - 整数 2.指针 - 指针 3.指…

华为云Astro大屏连接器创建操作实例:抽取物联网iotda影子设备数据的连接器创建

目录 样图(API连接器创建成功) 说明 操作场景(以Astro大屏抽取iotda影子参数为例) 实际操作步骤 新建连接器 设置基本信息。 接口鉴权方式,支持API鉴权、AK/SK、API Key和无身份验证 无身份验证 AK/SK认证(目前暂不能用) API Key认证(第三方使用) API鉴权认…

【硬件系统架构】哈佛架构

一、引言 在计算机科学的浩瀚宇宙中&#xff0c;计算机体系结构犹如星辰般繁多且各有独特光芒。哈佛架构便是其中一颗耀眼的明星&#xff0c;它在众多计算机体系结构中占据着独特而重要的地位。从计算机技术的萌芽期一路走来&#xff0c;哈佛架构不断发展演变&#xff0c;在不同…

华为eNSP:IS-IS认证

一、什么是IS-IS认证&#xff1f; 华为eNSP中的IS-IS认证 IS-IS认证是华为eNSP网络中用于保障中间系统到中间系统&#xff08;IS-IS&#xff09;协议通信安全性的核心机制&#xff0c;通过身份验证和数据完整性校验防止非法路由信息注入或篡改。其实现方式与关键特性如下&…

如何创建并使用极狐GitLab 项目访问令牌?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 项目访问令牌 在极狐GitLab 16.1中引入添加了默认前缀。 项目访问令牌类似于密码&#xff0c;但你可以 限制访问资源&#xf…

C# 异步详解

C# 异步编程详解 一、异步编程基础概念 1. 同步 vs 异步 ​​同步(Synchronous)​​&#xff1a;任务按顺序执行&#xff0c;前一个任务完成后才会执行下一个​​异步(Asynchronous)​​&#xff1a;任务可以非阻塞地启动&#xff0c;主线程可以继续执行其他操作 2. 异步编…

C++ 之 【模拟实现 list(节点、迭代器、常见接口)】(将三个模板放在同一个命名空间就实现 list 啦)

1.前提准备 (1) list 的底层结构一般是带头双向循环链表 (1)为避免命名冲突&#xff0c;需要创建一个命名空间来存放模拟实现的 list (2)下面模拟实现list时&#xff0c;声明和定义不分离(具体原因后续讲解) 2.完整实现 2.1 链表节点 template<class T>//节点写成类模板…

解决Win10虚拟机“网络连接不上”,“Ethernet0 网络电缆被拔出”的问题

一、情景引入 今天用Win10虚拟机打开浏览器发现&#xff1a; 很奇怪&#xff0c;平常都没有这个问题。 二、检查网络状态 点击更改适配器选项&#xff0c;发现如下&#xff1a; 三、解决问题 打开任务管理器&#xff0c;点击服务&#xff0c;搜索栏搜索&#xff1a;VM …

2025蓝桥杯省赛网络安全组wp

文章目录 黑客密室逃脱ezEvtxflowzipEnigma星际xml解析器EBC-TrainAES-CBC 黑客密室逃脱 提示猜文件名&#xff0c;猜几个常见的&#xff0c;app.py读到源码 这里也是脑抽了一下&#xff0c;把密钥看成1236了。。。卡了五分钟左右&#xff0c;解出来的时候已经降到300多分了&a…

算法查找目录

1. 基础数据结构 数组与链表 动态数组 实现与自动扩容机制均摊分析ArrayList/Vector实现 单向链表 基本操作(插入、删除、查找)链表反转环检测(Floyd判圈算法) 双向链表 插入删除操作优化双向遍历优势边界情况处理 循环链表 约瑟夫环问题单向循环链表双向循环链表 跳表 基本原…

Windows11 管理员用户下无权限操作的解决方法

问题 Program Files 目录下无权限进行写入操作。 Windows 各种功能因权限不足无法访问。 启动某些应用程序时&#xff0c;可能会遇到 用户账户控制 (UAC, User Account Control) 弹窗提示&#xff0c;要求确认是否允许该程序对设备进行更改。 等等问题 解决方法 运行 p…

.NET中,const和readonly区别

在.NET中&#xff0c;const和readonly都用于定义不可变的值&#xff0c;但它们在行为和使用场景上有显著区别。以下是两者的详细对比&#xff1a; 初始化时机 • const ◦ 编译时常量&#xff0c;必须在声明时赋值。 ◦ 值在编译时确定&#xff0c;并被直接嵌入到IL代码中&…

邮件分类特征维度实验分析

活动发起人小虚竹 想对你说&#xff1a; 这是一个以写作博客为目的的创作活动&#xff0c;旨在鼓励大学生博主们挖掘自己的创作潜能&#xff0c;展现自己的写作才华。如果你是一位热爱写作的、想要展现自己创作才华的小伙伴&#xff0c;那么&#xff0c;快来参加吧&#xff01…