当前位置: 首页 > news >正文

Linux线程——线程控制及理解

文章目录

  • 线程控制及理解
    • 简单验证概念结论
    • 线程库pthread的引入
      • pthread第三方库
      • 语言线程库
    • 线程控制的接口
      • 创建新线程 -> pthread_create
      • 获取线程的标识符 -> pthread_self
      • 终止线程 -> pthread_exit / pthread_cancel
      • 等待线程 -> pthread_join
      • 线程分离
    • 代码样例——结合接口综合使用
      • 提出几个细节
    • 线程库原理
      • 线程库的动态链接
      • 深入了解线程库内部
      • 传参/接收返回值相关问题
      • 线程库联动 & Linux底层实现
    • 线程局部存储
    • 多线程代码样例

线程控制及理解

本篇文章,我们将学习如何控制Linux下的线程,并且了解线程使用背后的原理!

简单验证概念结论

我们先不讲解如何使用,我们先来看一份代码,我们需要先验证一些结论:

1.线程本质是轻量级进程 -> 那么一个进程下的所有线程的pid应该相同
2.线程是共享同一个进程地址空间,即共享资源! -> 共享资源线程都能看到、
3.线程一旦出现异常、那么整个进程都会退出

我们根据上述的三个结论,来进行初步地验证:

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>void* newThreadRun(void* args){string tmp = (char*)args;   while(1){cout << "new thread : my name is " << tmp << endl;sleep(1);}//先随便返回一个值return (void*)0;
}int main(){pthread_t tid;//创建一个线程,让该线程执行newThreadRun函数pthread_create(&tid, nullptr, newThreadRun, (char*)"thread_1");while(1){cout << "main thread..." << endl;sleep(1);}return 0;
}
threadTest.exe:threadTest.cppg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -rf threadTest.exe

测试代码是否可以运行:
在这里插入图片描述


这里需要特别说明一个点:

这里使用的系统是Ubuntu 22.04,在这个情况下,直接对c++代码编译是可以直接使用pthread内的相关接口的。这可能是系统比较新,线程库pthread已经被加入到标准c库了。


但是,pthread.h内的接口,不是系统调用,而是语言层封装的!所以,如果是比较老的系统版本,使用该库的时候,编译必须要带上选项-lpthread具体原因后序讲解!


这里来简单的验证一下上面提到的几个结论:
1.我们来验证一下是否现成的pid都相同。如果是的话,是否有类似于pid一样的id来描述线程?
在这里插入图片描述
很明显,我们从监控窗口中可以明显发现,当前只有一个进程(pid为1275950)。

但是,在内核中,是否有描述线程的独立id呢?
答案是有的,我们需要通过指令ps -aL来查看:

在这里插入图片描述
其中,这个LWP就是内核中描述线程的唯一标识符id!
LWP(Light Weight Process),轻量级进程!这证明了Linux下线程就是轻量级的进程!

2.线程间是共享资源的

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>int flag = 100;void* newThreadRun(void* args){string tmp = (char*)args;   while(1){cout << "new thread : my name is " << tmp << endl;++flag;sleep(1);}//先随便返回一个值return (void*)0;
}int main(){pthread_t tid;//创建一个线程,让该线程执行newThreadRun函数pthread_create(&tid, nullptr, newThreadRun, (char*)"thread_1");while(1){cout << "main thread..." << ", flag is " << flag << endl;sleep(1);}return 0;
}

创建出的新进程不断地修改flag的值,然后main线程查看这个值是否改变:
在这里插入图片描述
这里因为访问的是共享数据,但是又没有对数据进行保护,所以会出现一些比较奇怪的情况。后面的内容会专门讲解Linux下线程如何保护数据。

这里我们只需要能够知道结论是正确的即可——线程共享同一份地址空间和资源!

3.单个线程出现异常,进程中所有线程都退出
这里就不展示代码了,直接在新创建的线程中加入一个除0错误即可:
在这里插入图片描述
我们确实可以发现,进程中所有线程都退出了。这也就是为什么我们直接通过ctrl + c发送信号时,所有线程都退出!

本质原因是:
因为信号机制,是基于进程的! 所以,学习信号的时候,我们总是在说进程信号
现在对于进程的理解是:一个进程下会有很多个线程。同时,线程共享的资源中,就包括了如何处理信号!所有线程对于信号的处理动作是一样的!

这也就是为什么,线程机制的引入,会导致代码的健壮性变低!

线程库pthread的引入

这里,我们需要正式的介绍一下在Linux系统下,我们使用的线程库——pthread。
在验证结论部分的代码中,创建线程的接口pthread_create就是pthread库内的。


Linux下,线程库的名称为POSIX 线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
• 要使用这些函数库,要通过引入头文件 <pthread.h>
• 链接这些线程函数库时要使⽤编译器命令的“-lpthread”选项

但是,前面说到过:
在一些比较新的系统中,这个-lpthread选项,即动态链接pthread库,是不用加的!因为比较新的系统已经把线程库加在标准库下了我们使用ldd指令都查不到。

但是,后续的讲解中,我都将以老内核版本来进行讲解,这样能够加深对线程库的理解!


pthread第三方库

如果我们通过man手册来查pthread.h下的一些相关函数,我们可以发现,都在3号手册:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


细节:最后都需要 Compile and link with -lpthread,即手动链接pthread库。比较新的系统不用!但是后续的所有讲解,都将基于老版本的系统进行讲解。

在3号手册,意味着这些函数是经过语言层封装的!并不是直接的系统调用。

那么,这里就会提出一个问题:
为什么Linux内部不提供关于线程操作的系统调用呢?为什么操作线程要用第三方库?

其实本质就是因为:Linux系统底层,根本就没有线程的概念,只有轻量级进程!
Linux系统下,没有线程的概念,线程的概念是由操作系统学科提出的!只不过说,Linux系统采用的是轻量级进程来模拟出线程的概念。每个系统的具体实现又是不一样的!

但是,所有的具体操作系统,都是要满足操作系统学科的指导思想!所以,Linux底层实现是这么一回事。如果不提供库,那就需要理解Linux底层对于线程的实现!这是非常不利于系统进行推广发展的!为此,操作系统开发者们,在用户层和内核中加了一个软件层,其实就是线程库。把对线程的操作封装成一些标准化的接口,方便用户使用,一定程度上忽略底层实现!

所以,使用Linux的线程,是需要搭配Linux开发者提供的线程库pthread动态库的,使用的时候动态链接!所以,pthread库也被称为原生线程库!

语言线程库

其实,很多语言也是有提供线程库的。如c++,在c++11标准后就正式提供了线程库!
在这里插入图片描述
当然,这里要说明的是,在比较老版本的内核下,就算我们包含了头文件thread,如果编译的时候不带选项-lpthread,也是没有办法使用这个线程库的!
(Ubuntn 22.04版本演示不了这个情况,所以这里只能进行说明!!!)

这是因为,在Linux系统下,c++线程库就是对原生线程库pthread进行封装的,并且进行面向对象化,方便用户使用!

而在Windows系统下,因为Windows系统下对于线程的实现方式是和Linux不同的。Windows系统是把线程当作独立的数据结构,所以会有专门使用线程的系统调用接口!所以,在Windows下使用c++线程库,其实是封装了Windows线程的系统调用接口!


这里就体现出来跨平台的差异了。但是,c++通过风转发,使用统一化标准的接口。使得这些代码在不同的平台下也能跑,即有非常好的可移植性!
当然,一个库想要在不同平台下跑:
方法是 -> 把所有的系统的使用方式都写到源码 + 条件编译进行动态裁剪!

下面,我们简的使用一下c++的线程库,不做讲解(后序我们会自行对接口封装面向对象化):

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <thread>void* Thread_1_run(void* args){string name = static_cast<char*>(args);cout << "i am a new thread, name is " << name << endl;sleep(1);return (void*)0;
}// 使用c++11给出的线程库
int main(){thread t(Thread_1_run, (char*)"thread_1");int cnt = 5;while(cnt--){cout << "main process" << endl;sleep(1);}// 类似于父进程回收子进程,这里是主线程回收新线程t.join();return 0;
}

在这里插入图片描述

线程控制的接口

这个部分,我们来一起了解一下线程控制的相关接口。这里我们讲解的是原生线程库的使用
还有始终要注意的是:系统版本比较老的话,需要编译时带上选项-lpthread使用!

创建新线程 -> pthread_create

NAMEpthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, 
and the contents of *thread are undefined.
  1. 这个接口的第一个参数thread是线程的一个独有标识,类型为pthread_t,这是一个输出型参数。也就是说,我们需要手动传入一个该类型变量的地址,该接口调用的时候,会修改传入的变量的内容,这个内容就是开辟的线程的唯一标识!
  2. 第二个参数是创建线程的状态的相关设置。我们可以自行设置创建的线程的相关状态。但是一般来说,这个我们直接使用默认设置即可,即传入nullptr
  3. 第三个参数start_routine就是需要被回调的函数的指针,即我们希望新创建的线程执行哪一个函数!
    (该函数,返回值为void*,参数也为void*)。
  4. 第四个参数arg就是回调函数的参数,即回调函数的参数部分进行接收使用!这是一个空指针类型,是可以接收任意变量的地址的!

如果线程创建成功,返回值为0;如果失败,就会返回一个错误码,且*thread对应的线程未定义!
注意,不是全局变量的errno!

获取线程的标识符 -> pthread_self

NAMEpthread_self - obtain ID of the calling threadSYNOPSIS#include <pthread.h>pthread_t pthread_self(void);Compile and link with -pthread.RETURN VALUEThis function always succeeds, returning the calling thread's ID.

这个很简单,哪个线程调用该函数,就返回该线程的标识符ID。这个函数始终都是成功的!因为只要使用它,就必然是有线程在调用该函数!

终止线程 -> pthread_exit / pthread_cancel

pthread_exit

NAMEpthread_exit - terminate calling threadSYNOPSIS#include <pthread.h>void pthread_exit(void *retval);Compile and link with -pthread.RETURN VALUEThis function dos not return to the caller.

如果说,想要某个线程直接退出,就需要使用这个接口,而不是直接使用exit()
我们来试一下使用exit会造成什么后果:
在这里插入图片描述
我们会发现,整个进程中的所有线程都会直接退出。因为exit的本质就是,通过系统调用_exit,主动陷入内核,触发中断。处理该中断的方法就是向进程发出退出信号。所以整个进程都退出了!

如果只是想让单一的线程退出,就需要使用pthread_exit!

这里解释一下pthread_exit的参数void retval,这个其实类似于退出码!如我们使用的exit(23)>一样。但是这里是void类型,即可以接收任意地址的变量,如(void*)0


pthread_exit是线程自己退出。当然,也可以让主线程主动取消新线程:


NAMEpthread_cancel - send a cancellation request to a threadSYNOPSIS#include <pthread.h>int pthread_cancel(pthread_t thread);Compile and link with -pthread.RETURN VALUEOn success, pthread_cancel() returns 0; on error, it returns a nonzero error number.

使用很简单,只需要主线程传入需要被取消的线程id即可!

等待线程 -> pthread_join

NAMEpthread_join - join with a terminated threadSYNOPSIS#include <pthread.h>int pthread_join(pthread_t thread, void **retval);Compile and link with -pthread.RETURN VALUEOn success, pthread_join() returns 0; on error, it returns an error number.

线程,一定是由另外一个线程创建的。所以,会有主线程和新线程之分!如果主线程不进行等待线程,那么会出现类似于僵尸进程一样的情况,但注意,没有僵尸线程的说法!


我们来演示一下:

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>// 使用原生线程库 -> 演示线程内存泄露void* thread_1_run(void* args){string name = static_cast<char*>(args);// 新线程只跑5sint cnt = 5;while(cnt--){cout << "i am a new thread, name is " << name << endl;sleep(1);}cout << name << "线程结束" << endl;return (void*)0;
}int main(){pthread_t tid;pthread_create(&tid, nullptr, thread_1_run, (char*)"thread_1");// 让主线程一直不退出while(1){cout << "main process" << endl;sleep(1);}return 0;
}

同时,为了观察到现象,我们在xshell终端搭建一个监控窗口:

while  :; do ps -aL | head -1 && ps -aL | grep threadTest.exe | grep -v grep ; 
sleep 1 ; done

正常来说,是可以看得到资源泄露的,即线程thread_1即使退出了,但是通过监控窗口还是能看到其存在于系统中(主线程一直不退)。但是,由于我的系统(Ubuntu 22.04太新了),对于线程的回收做了优化,即使不显示调用pthread_join,也会回收新线程。但是为了跨平台的可移植性考虑,以及规范编写代码,最好是显示调用该接口,进行线程的等待!

线程分离

正常情况下,被创建的线程是需要等待回收的!(当然,新的内核版本做了优化,这个我们不考虑)。
这种情况,我们称为线程的状态为joinable,即默认需要等待回收!

但是,有些情况下是不太关心的退出情况的!这种时候,就需要提供一种方式来忽略线程的回收。
就像进程信号中,对SIGCHLD的处理动作设置为忽略,这样子父进程就不用回收子进程了!


所以,在Linux系统下,对于线程是否需要被回收,可以通过设置现成的状态来控制:

joinable,线程默认状态,需要等待回收
!joinable 或者 detach,该进程不需要回收,即和主线程分离

NAMEpthread_detach - detach a threadSYNOPSIS#include <pthread.h>int pthread_detach(pthread_t thread);Compile and link with -pthread.RETURN VALUEOn success, pthread_detach() returns 0; on error, it returns an error number.

这个接口是很简单的,直接传入要分离的线程即可!可以在主线程控制要分离哪个线程,也可以在某个线程调用的函数内自行分离,pthread_detach(pthread_self())。

如果线程detach了,那么等待回收的时候就会出错!

成功返回0,反之返回一个错误码。这个错误码不是全局变量errno


这里还要说的是:
线程虽然分离,但是还是会共享资源的!只不过是不再需要等待主线程的回收罢了!

代码样例——结合接口综合使用

在这里,我们将综合地使用一下上面介绍到的接口,点出一些细节,方便后序理解原理!

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <thread>void* thread_1_run(void* args){//这里也可以直接从线程中分离 ,效果和主线程中分离一样//pthread_detach(pthread_self()); //这里就不重复展示了string name = static_cast<char*>(args);// 新线程只跑5sint cnt = 5;while(cnt--){cout << "i am a new thread, name is " << name << endl;cout << name << "线程id : " << pthread_self();sleep(1);}// 直接让线程终止,把3强制转化为void*当作pthread_exit的参数cout << name << "线程结束" << endl;pthread_exit((void*)3);return (void*)0;
}int main(){//创建一个线程 -> pthread_createpthread_t tid;pthread_create(&tid, nullptr, thread_1_run, (char*)"thread_1");//新线程执行thread_1_run,主线程继续往下执行//打印主线程的线程id// 分离线程thread_1/* cout << "分离线程thread_1" << endl;pthread_detach(tid); */int cnt = 10;while(cnt--){cout << "main thread id : " << pthread_self() << endl;sleep(1);}// pthread_join的第二个参数是void**,是一个输出型参数!  // 就是把线程退出返回的变量以void* 返回,但是为了能够接收到,就只能通过传址调用来进行修改外界变量!void* ret;pthread_join(tid, &ret);cout << "thread_1 退出结果" << (long long)ret << endl;return 0;
}// 注意,线程中都有变量cnt,这个是属于每个线程的栈区的,互不影响!!!

在这里插入图片描述

我们可以发现,退出码可以通过void*变量来设置!但是接收到这个退出信息还是比较麻烦的,需要通过输出型参数的方式来进行返回。(但是,讲到原生线程库的原理的时候就很容易明白了)!
但是,将返回结果转整形的时候,需要转为长整型long long

让线程分离:
在这里插入图片描述

提出几个细节

1.上述的操作中,我们可以发现,回调函数的参数的用的是(void*),线程退出的时候,返回值也要通过(void*)设置,回收线程获得返回结构,也是需要通过传入(void*)的地址,通过参数输出来接收!
既然是用(void*),那么就说明,传入的参数肯定可以是其它的,比如上面代码样例中,我们将3强转为(void*)返回,最后再转整形就能得到3这个退出码!

-> 那我们是否可以传入一个类呢?答案是,当然可以!
我们在这里,还是通过一个代码样例来验证:

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>class Task{
public:Task(int a = 0, int b = 0):_a(a), _b(b){}~Task(){}int Execute(){return _a + _b;}private:int _a;int _b;
};class Result{
public:Result(int res = 0):_res(res){}~Result(){}int GetRes() const{return _res;}
private:int _res;
};void* thread_1_run(void* args){// 此时该线程知道,主线程传入的是Task*Task* pt = static_cast<Task*>(args);Result* res = new Result(pt->Execute());// 新线程只跑5sint cnt = 5;while(cnt--){cout << "i am a new thread" << " ,线程id : " << pthread_self() << endl;sleep(1);}return res;
}int main(){// 使用堆区上的,使用栈区上也可以,但是还是用堆区上的规范一些 -> 讲解原理后明白Task* ptask = new Task(10, 20);pthread_t tid;// 把类传入pthread_create(&tid, nullptr, thread_1_run, ptask);cout << "主线程等待回收新线程" << endl;void* res = nullptr;pthread_join(tid, &res);// 此时就把Result类返回了Result* pr = static_cast<Result*>(res);cout << "任务返回结果 : " << pr->GetRes() << endl;delete ptask;delete pr;return 0;
}

在这里插入图片描述
所以,在未来,创建线程的时候可以任意传参。返回线程的时候也可以传参!


2.线程id(pthread_self()获取),为什么看着那么大?其本质是什么?为什么不能直接用LWP?
这里先简单解释为什么不能直接用LWP。还是一样的原因,因为Linux系统中根本就没有线程的概念,只有LWP,即轻量级进程!如果直接提供LWP给用户层使用,用户还是得了解Linux的底层实现!所以,线程的id,不应该是LWP,也不能是!

至于其本质是什么,这里我们先放着,等讲解线程库原理的时候再详谈!


3.既然说线程分离就是设置状态,能不能验证一下这个结论?
这个我们需要从源码中来查看,这个部分将会放在后续的附录中!

线程库原理

至此,我们是看出来一个结论:即Linux系统下没有线程,只有轻量级进程!但是,Linux系统究竟是如何使用这个原生线程库的呢?


线程库的动态链接

我们知道,线程库本质上也是一个文件,格式为ELF!我们自己写的所有可执行程序,也是ELF。所以,最终它们都会被平坦编址,然后通过映射到进程地址空间上的基址 + 偏移量,就可以找到对应代码。

但是,对于动态库来说,是选择动态链接的,运行的时候将动态库映射到共享区:
在这里插入图片描述
进程/线程执行代码的时候,通过全局GOT表和PLT机制,跳转到动态库的位置执行线程库代码!

深入了解线程库内部

对于动态库如何使用,如何跳转,我们都是很清晰的。但是,为了能够更深刻地理解线程库的使用,也是为了解决前面的问题,我们需要深入了解线程库内部!


线程,其实是操作系统学科提出的概念!操作系统没有!但是实际上,Linux却是通过轻量级进程来模拟实现进程的!所以,所有的进程,都应该会有自己相关的数据。

只不过说,因为Linux系统底层没有线程的概念,所以对于线程的一些相关数据,并不是在内核中维护的!而是在原生线程库内部进行维护的!结构体大致如下:

struct pthread{// 线程id// 线程状态(joined, detach)// 线程的独立栈// 线程栈的大小// 线程其他的相关数据属性...
};//这些数据内核中没有,用户层才会使用!!!

所以,在线程库这个动态库中,会存在着很多这样的描述线程的结构体,每个结构体情况又不一样,那么库中也必须要对这么多个线程进程管理!方法也是先描述,再组织!

这里要提出一个问题:
为什么动态库里面也可以先描述,再组织呢?这为什么能用呢?
回答这个问题很简单,我们参考c语言文件结构体FILE*!内核数据结构中已经有了文件对应的描述数据结构了,但是语言层还是自己实现了!本质上也是c语言文件库中在进行描述和组织!


所以,本质原因就是,我们的代码再调用库中的相关操作!比如fopen就是打开一个文件,其实在语言层就是添加一个FILE,然后加入到数据结构。创建线程也是一样的!!!


线程库中组织线程的大致结构
在这里插入图片描述
mmap区域就是共享内存区域,是POSIX标准的共享内存!大概了解一下即可。

通过这张图我们大致能够发现,在进程地址空间的mmap区域,存放的就是动态库的代码,和一系列管理进程的数据块!这个数据块由三部分组成:线程的结构体(TCB)、线程栈、还有线程的局部存储!
组织结构就类似于一个数组,存放在里面。

TIPS:
这里只是为了理解方便,才这么画每个数据块的空间。其实,每个数据块中的空间是动态开辟的!

所以,所谓的进程ID本质,其实就是每个数据块的起始地址罢了,就是进程地址空间上的一个地址!地址也确实是有唯一性。所以,这就是为什么进程id的值会看的那么大。一般是用16进制表示的!

传参/接收返回值相关问题

我们肯定会好奇,为什么线程的传参和接收返回内容是如此的麻烦。
这是因为,在struct pthread(TCB)中,有个变量叫做void* res。这是用来存储线程返回值的!

线程合法退出有几种情况:

1.线程回调函数执行return语句
2.线程使用pthread_exit退出
3.主线程使用pthread_cancel取消线程

线程退出的时候,线程库的代码就会自动地将要返回的内容的地址,让TCB中的void* res指向。所以,当我们真的需要接收返回值的时候,就需要通过一个输出型参数(void*),将其地址写入,把其指向的内容做修改,这样子才能拿到退出信息(本质就是传址调用)。

线程库联动 & Linux底层实现

首先,我们这里输出一个结论:
每个线程都有自己独立使用的栈(但其它的线程想要看到也是可以的)。线程的栈和主线程的栈不一样,是固定的,不会生长!用完了就没了!

现在会面临一个问题,既然每个线程的相应数据结构是在线程库中出现的,那么问题来了:
系统是如何做到调用代码的时候和线程库部分的代码进行联动使用的呢?


首先,Linux系统下,CPU调度的基本单位是LWP!也就是说,CPU一直在不断地通过分时操作来调用一个又一个的LWP!

这里要补充一个知识点:
一个进程的单次调度是有时间片的!注意,这是给进程用的!如果有多个线程共享同一个进程地址空间,那么所有的线程会共享这一个时间片!
道理很简单:就是预防有些程序恶意创建线程,如果时间片每个线程都一样,那么这对于内核来说是很危险的!


所以,真正进行调度的时候
1.创建一个线程,线程是有其对应的执行函数的,然后就会跳转到动态库的部分(GOT表 + PLT机制),在线程库部分中创建对应的TCB!

2.线程库的线程创建函数是语言层封装了系统调用的:
在Linux系统下,可以使用接口vforkclone创建指向同一个地址空间的LWP!所以,在线程库代码中,又会主动触发中断,让操作系统创建对应的LWP!代价比一般单线程进程小得多!

3.CPU调度LWP的时候,会通过代码区中定义的函数来执行!然后每个线程在共享区中被管理起来,有自己的局部存储空间和独立使用的栈!起始本质上,这些数据都有地址。最终要使用的时候都能找得到地址来进行调用!


所以,在Linux系统下,LWP和用户级进程是1 : 1的,当然有些系统可能是1 : n。但是我们只关注Linux系统下的情况。至此,我们就能很清楚知道系统是如何调用线程库的了!

线程局部存储

这里我们来讲解一下线程局部存储的问题:

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>// 栈的局部存储int cnt = 1;void* thread_1_run(void* args){string name = static_cast<char*>(args);while(1){cout << name << "线程修改cnt变量, cnt : " << cnt << endl;++cnt;sleep(1);}return nullptr;
}void* thread_2_run(void* args){string name = static_cast<char*>(args);while(1){cout << name << "线程查看cnt变量, cnt : " << cnt << endl;sleep(1);}return nullptr;
}int main(){pthread_t tid1, tid2;pthread_create(&tid1, nullptr, thread_1_run, (char*)"thread_1");pthread_create(&tid2, nullptr, thread_2_run, (char*)"thread_2");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);return 0;
}

这段代码就是线程1修改全局变量cnt,线程2在查看,直接看结果:
在这里插入图片描述


但是,如果说我们在cnt变量前面加上__thread(前面是两个杠),我们来看看结果:
在这里插入图片描述
惊奇地发现,thread_1不断地修改变量,但是thread_2在查看的时候,竟然没有查到修改。
这就是线程局部存储的概念!
也就是说,如果需要使用某个全局变量,但是又不希望其他线程能看看到使用情况,就可以放在线程局部存储上!

Linux栈的局部存储:
就是某个线程想使用全局变量性质,但又不希望别的线程看到。所以就有了栈的局部存储,起始就是每个线程的控制块中有一个位置,把全局变量拷贝下来。但是,又保留了其全局性,只能由对应的线程修改!


__thread其实是一个编译器选项,在某个变量前使用:
意思就是告诉编译器,该变量需要使用线程局部存储的方式来使用!

但是还是有需要注意的点:

1.线程局部存储只能用于一些内置类型的变量!
2.只能用于部分的指针!

多线程代码样例

前面都是只创建一个线程在操作,那现在我们可以思考,是否可以一次性创建多个线程呢?

#include <iostream>
using namespace std;
#include <pthread.h>
#include <unistd.h>
#include <string>
#include<vector>// 多线程的使用// 这里简单一点,直接让每个线程都执行一样的任务即可
void* thread_run(void* args){sleep(1);string name = static_cast<char*>(args);cout << name << "线程沉睡1s, 现在开始运行!" << endl;// 线程id的本质是线程控制块的起始地址,这个数字太大,把它转化为16进制char tid[64];// tid为长整型,所以需要加"l"snprintf(tid, sizeof(tid), "0x%lx", pthread_self());int cnt = 5;while(cnt--){cout << "name[" << name << "]" << ", thread["  << tid << "]" << endl;sleep(1);}return nullptr;
}// 默认创建的线程数
const int thread_num = 10;int main(){vector<pthread_t> _tid_vector(thread_num, 0);for(int i = 0; i < 10; ++i){// 每个线程的名字 -> 但是这里可能会出现问题char name[64];snprintf(name, sizeof(name), "thread_%d", i);// 创建线程int n = pthread_create(&_tid_vector[i], nullptr, thread_run, name);if(n == 0)cout << name << "线程创建成功!" << endl;else {cerr << name <<"线程创建失败, 将创建下一个进程" << endl;continue;}// 这里建议每个线程的创建时间间隔稍微长一点:// 因为现在那么多个线程都对显示器文件输出,显示器文件本质也是共享资源// 这里我们并没有对共享资源做保护,如果时间间隔过短,可能会导致打印出来的内容不符合预期!sleep(3);}// 此时,_tid_vector中已经存储了所有的创建出来的新线程的tid,一一等待回收即可for(int i = 0; i < thread_num; ++i){int rd = pthread_join(_tid_vector[i], nullptr);if(rd == 0) cout << "thread_" << i << "回收成功" << endl;else cerr << "thread_" << i << "回收失败" << endl;}return 0;
}

运行结果:

ynp@hcss-ecs-1643:~/linux-learn-code/2025_8_10/ThreadFuncTest$ ./threadTest.exe 
thread_0线程创建成功!
thread_0线程沉睡1s, 现在开始运行!
name[thread_0], thread[0x7f78bdee6640]
name[thread_0], thread[0x7f78bdee6640]
thread_1线程创建成功!
name[thread_0], thread[0x7f78bdee6640]
thread_1线程沉睡1s, 现在开始运行!
name[thread_1], thread[0x7f78bd6e5640]
name[thread_0], thread[0x7f78bdee6640]
name[thread_1], thread[0x7f78bd6e5640]
name[thread_0], thread[0x7f78bdee6640]
thread_2线程创建成功!
name[thread_1], thread[0x7f78bd6e5640]
thread_2线程沉睡1s, 现在开始运行!
name[thread_2], thread[0x7f78bcee4640]
name[thread_1], thread[0x7f78bd6e5640]
name[thread_2], thread[0x7f78bcee4640]
name[thread_1], thread[0x7f78bd6e5640]
thread_3线程创建成功!
name[thread_2], thread[0x7f78bcee4640]
thread_3线程沉睡1s, 现在开始运行!
name[thread_3], thread[0x7f78bc6e3640]
name[thread_2], thread[0x7f78bcee4640]
name[thread_3], thread[0x7f78bc6e3640]
name[thread_2], thread[0x7f78bcee4640]
thread_4线程创建成功!
name[thread_3], thread[0x7f78bc6e3640]
thread_4线程沉睡1s, 现在开始运行!
name[thread_4], thread[0x7f78bbee2640]
name[thread_3], thread[0x7f78bc6e3640]
name[thread_4], thread[0x7f78bbee2640]
name[thread_3], thread[0x7f78bc6e3640]
thread_5线程创建成功!
name[thread_4], thread[0x7f78bbee2640]
thread_5线程沉睡1s, 现在开始运行!
name[thread_5], thread[0x7f78bb6e1640]
name[thread_4], thread[0x7f78bbee2640]
name[thread_5], thread[0x7f78bb6e1640]
name[thread_4], thread[0x7f78bbee2640]
thread_6线程创建成功!
name[thread_5], thread[0x7f78bb6e1640]
thread_6线程沉睡1s, 现在开始运行!
name[thread_6], thread[0x7f78baee0640]
name[thread_5], thread[0x7f78bb6e1640]
name[thread_6], thread[0x7f78baee0640]
name[thread_5], thread[0x7f78bb6e1640]
thread_7线程创建成功!
name[thread_6], thread[0x7f78baee0640]
thread_7线程沉睡1s, 现在开始运行!
name[thread_7], thread[0x7f78ba6df640]
name[thread_6], thread[0x7f78baee0640]
name[thread_7], thread[0x7f78ba6df640]
name[thread_6], thread[0x7f78baee0640]
thread_8线程创建成功!
name[thread_7], thread[0x7f78ba6df640]
thread_8线程沉睡1s, 现在开始运行!
name[thread_8], thread[0x7f78b9ede640]
name[thread_7], thread[0x7f78ba6df640]
name[thread_8], thread[0x7f78b9ede640]
name[thread_7], thread[0x7f78ba6df640]
thread_9线程创建成功!
name[thread_8], thread[0x7f78b9ede640]
thread_9线程沉睡1s, 现在开始运行!
name[thread_9], thread[0x7f78b96dd640]
name[thread_8], thread[0x7f78b9ede640]
name[thread_9], thread[0x7f78b96dd640]
name[thread_8], thread[0x7f78b9ede640]
thread_0回收成功
thread_1回收成功
thread_2回收成功
thread_3回收成功
thread_4回收成功
thread_5回收成功
thread_6回收成功
thread_7回收成功
name[thread_9], thread[0x7f78b96dd640]
thread_8回收成功
name[thread_9], thread[0x7f78b96dd640]
name[thread_9], thread[0x7f78b96dd640]
thread_9回收成功

这里,我们就成功的使用多线程来进行一些简单项目的操作了!


但是,这里我们来验证一个错误:
在这里插入图片描述
即我们把进程创建的间隔时间3s给取消掉了,我们会发现,所有的线程名字都是thread_9
这非常奇怪,这是为什么呢?

其实很简单。因为线程名字是由主线程提供的,即char iname64]。这是一个栈区上的临时变量。每次进入循环的时候为这个数组创建空间,单词循环结束这个空间又没了。
但是,这里最大的问题是,每个线程在运行的之前,都会sleep(1),而线程的创建又没有时间间隔。CPU的运行速度是很快的,线程创建完后还要等上一会儿,第一个线程才开始运行。而线程运行的函数中,void* args指向的就是char name[64]。而因为线程创建完了,那么该数组的内容早就被不断地覆盖写入,那么最后肯定是最后一个名字!

所以这就导致,后续的所有线程得到的名字都是thread_9


解决方案很简单:
只需要每次进入循环的时候,数组不要在栈上进行开辟,而是在堆上进行开辟即可:
在这里插入图片描述
最后再把所有的空间释放掉即可。这样子,每个线程使用的名字存储的位置都是不一样的!这样子就不用担心线程的名字会有冲突了!

当然,这只是权宜之计。等到我们学习了如何保护共享资源的时候就能更好地解决这个问题!

http://www.lryc.cn/news/617860.html

相关文章:

  • Transformer前传:Seq2Seq与注意力机制Attention
  • Haystack:面向大模型应用的模块化检索增强生成(RAG)框架
  • 什么情况下会导致日本服务器变慢?解决办法
  • Linux kernel network stack, some good article
  • Flink + Hologres构建实时数仓
  • Spring JDBC
  • TDengine IDMP 基本功能(1.界面布局和操作)
  • 【华为机试】208. 实现 Trie (前缀树)
  • openGauss逻辑备份恢复工具gs_dump/gs_restore
  • AI生成代码时代的商业模式重构:从“软件即产品”到“价值即服务”
  • 大模型落地实践:从技术重构到行业变革的双重突破
  • 亚马逊广告底层逻辑重构:从流量博弈到价值创造的战略升维
  • 思科交换机的不同级别IOS软件有什么区别?
  • Oracle数据库中的Library cache lock和pin介绍
  • Qt——实现”Hello World“、认识对象树与Qt坐标系
  • 力扣109:有序链表转换二叉搜索树
  • Linux下安装jdk
  • 分享一款基于STC8H8K32U-45I-LQFP48单片机的4路数字量输入输出模块
  • STM32——system文件夹
  • Day12 Maven高级
  • 2025牛客多校第七场 双生、象牙 个人题解
  • 大模型提示词工程实践:大语言模型文本转换实践
  • python之uv使用
  • 深度学习和神经网络最基础的mlp,从最基础的开始讲
  • OpenBMC中的snk-psu-manager:架构、原理与应用深度解析
  • 排错000
  • HTML应用指南:利用GET请求获取全国一加授权零售店位置信息
  • 工业相机与智能相机的区别
  • 【05】昊一源科技——昊一源科技 嵌入式笔试, 校招,题目记录及解析
  • 【unity实战】在Unity中实现不规则模型的网格建造系统(附项目源码)