文章目录
前言:
在上一文的线程控制中,我们先是聊了关于为什么我们要在编译链接时将线程库给链接起来,简单回顾一下,其根本原因,就是因为我们理解的线程和Linux中的线程是两个不同的概念,在Linux操作系统中,只认"LWP"即轻量级进程,而为了能够很好的适配我们所理解的线程,线程库pthread就出现了。
然后紧接着我们学习了控制线程的一系列操作,包括线程的创建、线程的等待、线程的终止、线程的分离。
这部分的学习和当年我们学习进程的一系列操作一致。
那我们在这里想要简单回顾一下Linux下,进程和线程之间的关系:
特性 | 进程 | 线程 |
---|---|---|
定义 | 程序的执行实例 | 进程中的执行单元 |
内存 | 每个进程有独立的地址空间 | 线程共享进程的内存空间 |
资源 | 独立的资源(如文件描述符、内存) | 共享进程资源,独立的栈空间 |
调度 | 每个进程是内核调度的基本单位 | 线程是进程内部的调度实体 |
创建开销 | 较大(需要复制资源) | 较小(共享进程资源) |
并发/并行 | 进程可以并行执行,但开销较大 | 线程可以并发执行,开销小 |
简单封装一下C++线程库
为了能向后期学习C++11中的线程库看齐,我们在这里以前先自己模拟手搓一个线程库出来,那么说干就干,首先我们先出个简易的步骤:
- "先描述",一个简单简易的线程类,应该包含一个线程的名字 、tid 、运行状态 、要执行的回调函数。
- 构造函数直接打印一条消息即可,在main函数中创建线程使用emplace_back直接构造即可。
- 使线程执行操作,直接调用函数指针(这里会存在一个关于this指针的问题,一会聊)。
- 终止线程操作
- join等待回收线程。
-
构造函数及私有成员:
c++class My_Thread { // 构造函数 Thread(std::string name, Func_t func) : _name(name) , _func(func) { std::cout << _name << " have been created!" << std::endl; } // ... 其它操作 // 私有成员属性 private: std::string _name; // 名字 ("thread-1 ...") pthread_t _tid; // tid Func_t _func; // 回调函数地址 bool _isrunning; // 运行状态 };
-
执行线程
本质还是在这一步使用
pthread_create()
函数创建线程,然后通过其参数里的"函数指针"实现操作!但是!
因为咱们本质是在类里面调用的
pthread_create()
函数的,而若我们通过函数指针调用的函数也定义在该类步,那么就会出现报错!
原因就在于,Threadd_Routine()函数内部,还隐含了一个this指针!
解决办法也很简单,我们可以在该函数前面++添加static关键字++
简单回顾一下,当年学习C++时我们说过,添加static关键字修饰的函数,会存放在静态区,属于是其它类也可以用,而不是是独属某个类.
因此在这里我们可以修改修改:
c++void Execute() { _isrunning = true; _func(_name); _isrunning = false; } // 如果不加static修饰的话,该函数就是此类独有的,那这个函数的参数就会隐含一个this指针! // 加上static关键字进行修饰,就能保证该函数是存在于静态区的,不是某个类独有的! static void* Thread_Routine(void *args) { My_Thread *Self = (My_Thread *)args; Self->Execute(); // 在这里当然可以直接调用_func,但是代码健壮性太烂,不雅观 return nullptr; } bool Start() { // 创建线程 int n = ::pthread_create(&_tid, nullptr, Thread_Routine, this); // 因为Thread_Routine不独属于某个类,所以在这里传递"当前类"this. if (n != 0) return false; return true; }
-
其余部分(终止线程,等待回收线程):
c++void Stop() { while (!_isrunning) { ::pthread_cancel(_tid); _isrunning = false; std::cout << _name << " Stop" << std::endl; break; } } void Join() { if(!_isrunning) { int n = ::pthread_join(_tid, nullptr); if (n == 0) { std::cout << _name << " join suceess!" << std::endl; } } } ~My_Thread() {}
main函数编写:
c++
void Print(const std::string &name)
{
int cnt = 1;
while (true)
{
std::cout << name << " is running... cnt: " << cnt << std::endl;
cnt++;
sleep(1);
}
}
int main()
{
std::vector<My_Thread> threads;
for (int i = 1; i <= 5; ++i)
{
std::string name = "thread-" + std::to_string(i);
threads.emplace_back(name, Print);
sleep(1);
}
for (auto &t : threads)
{
t.Start();
}
sleep(10);
// 结束多线程
for (auto &t : threads)
{
t.Stop();
}
// 等待多线程
for (auto &t : threads)
{
t.Join();
}
return 0;
}
运行结果:
如何理解tid?
相信学到这里,你或多或少有对线程的LWP的编号和tid有疑问。那么别担心,这一部分我们会将tid以及LWP彻底讲解清楚!
-
LWP?PCB?
首先我们先来回顾一下,曾经我说过!------------ "Linux操作系统,在内核角度来看,只认识LWP(轻量级进程)"
所以其实在Linux内核中,没有我们曾将讲解的PCB,也就是进程控制块,这个是"不存在"的!
取而代之的,其实是一个一个的LWP,也就是轻量级进程。
-
tid?LWP?
我们在谈"创建线程"时,我们介绍了函数
pthread_create()
,他有一个参数名字是pthread_t *thread
,而如果创建成功了,这个thread就会获得值,这个值就代表的是++线程的ID++。可是,咱们上次创建了线程,查看到LWP也有一个值,类比一下进程的pid,是一个进程的标识。
那你通过
pthread_create()
创建后得到的thread,它也是线程的ID,那他是tid吗?如果是,那LWP的值又算什么呢?
理解pthread库:
为了能够较好的理解上述的tid和LWP的关系,我们首先需要先来认识理解pthread库。
还是一样,先回顾库是怎么加载的!
有一天你写好了一段关于创建线程的代码,这个代码会存放在磁盘上。
又有一天,你想跑这个代码了,于是你打开代码编译链接形参可执行程序的过程中,经历了这几个过程:
- 我将你的代码编译之后拿到++物理内存++中
- 在我进行查看链接时,我发现你需要链接libpthread.so这个库,于是我就从磁盘中把这个库也**++拿到物理内存++**中
- 接下来为了能让Linux内核也能看到,于是我把它通过页表,映射到虚拟地址空间中。具体的位置就位于堆栈之间的++共享区++
内核视角与用户视角:
基于上面的理解,我们能得出一个结论,那就是Linux操作系统内核是不维护我们认为的线程的!
本质上,Linux操作系统不认可线程,而是只在乎它自己的LWP,这也就是为什么"线程"也被称为++"用户级线程"++。
正因为要适配用户的需求,那我们就得来实现维护和封装。
那么我们又该怎么去维护,或者说管理我们的++用户级线程++的呢? ------ 先描述,再组织!
-
所以我们使用
pthread_create()
函数创建的用户级线程的相关属性,就都会存放在了这个动态库里 -
至此,我们可以总结 ------------ 记录线程的相关属性和操作都会被记录在这"线程控制块"内,其会被存放在动态库里!
-
而tid是什么? ------ tid就是该线程块的起始地址!
-
这个可能有点难理解,就想像我们在学习C++的unordered_map时,我们只是使用并不关心其内部的接口是怎么实现,我们也学过那些哈希表上的节点不也是malloc出来的吗,而这些节点其实就是在它的库里进行维护的!
-
其实新线程执行完后返回,在内部来看,其实LWP是被释放了的,但是库里面的用户级线程还没有被释放,它还记录着返回值信息,如果碰到主线程在join,那就会把返回值信息拷贝给当初的
void* ret
,然后自己再释放,这就是为什么当时我会说可能出现"僵尸线程"这种情况 -
用户级线程通过pthread_create创建,而pthread_create内部又会调用系统调用接口clone来创建系统级线程LWP
-
"线程控制块"里有一个线程栈,这就正好对应上我之前讲解过的,每个线程都有一套独属于自己的栈空间。
而这个线程栈,当然不是库里面的空间,它可能也是个指针,然后记录了多大的范围,告诉线程和Linux说,在虚拟地址空间中,你线程的栈空间就是这么大。
其实就一句话:
用户级线程其实就是一个在库函数里的结构体!