【Linux】线程概念和线程控制
一、理解线程什么是线程呢?下面我们直接说定义,再理解。线程就是进程内的一个实行分支,线程的实行粒度要比进程细。
1. Linux中的线程
下面我们开始理解一下Linux中的线程。我们以前说过,一个进程被创建出来,要有自己对应的进程PCB的,也就是 task_struct,也要有自己的地址空间、页表,颠末页表映射到物理内存中。所以在进程角度,我们能看到进程全部的资源时,现在就能通过地址空间来看,所以地址空间是进程的资源窗口!
以前我们谈的进程,它所创建的地址空间内的全部资源,都是由一个 task_struct 所享有的,那么页表也是属于它独有的。那么假如我们再创建一个“进程”,但是不再给这个“进程”创建新的地址空间和页表,它只需要在创建时指向“父进程”的地址空间。将来“父进程”就将代码区中的代码分一部门给这个“子进程”,以及别的数据分一部门给它,此时我们就可以让“父进程”在运行的时候“子进程”也在运行。那么该父进程能创建一个,就能创建许多个,如下图:
https://i-blog.csdnimg.cn/blog_migrate/78d5a696ed6bc8d65fa6f949be85d7c6.png
那么我们新创建出来的“子进程”,它们在实行粒度上要比“父进程”的实行粒度要更细一些,由于以前“父进程”需要实行全部代码,而这些“子进程”只需要实行一部门代码,所以,为了显着区分这些“子进程”和“父进程”,我们把这种形式的“子进程”,称为线程!
所以在 Linux 中,线程在进程“内部”实行,也就是线程在进程的地址空间内运行。那么它为什么要在进程的地址空间内运行呢?首先,任何实行流要实行,都要有资源!而地址空间是进程的资源窗口!
那么在 CPU 看来,它知道这个 task_struct 是进程还是线程吗?它需要知道吗?并不需要!由于CPU只有调度实行流的概念!
2. 重新定义线程和进程
那么有了上面的基础,我们现在重新定义线程和进程的概念。
[*]线程:我们认为,线程是操纵体系调度的根本单元;
所以什么到底什么是进程呢?我们以前说的进程指的是 task_struct 和代码数据,但是本日很显然已经有分歧了,由于它只是地址空间的一个实行分支,一个实行分支不能代表整个进程!那么我们现在需要重新理解一下了,全部 task_struct 实行流都叫做进程实行流,地址空间都叫做进程所占据的资源,页表和该进程所占用的物理内存,我们把这一整套才称之为进程!如下图:
https://i-blog.csdnimg.cn/blog_migrate/9e4b430ec19851cb09e7687728f49924.png
[*]进程:进程是承担分配体系资源的根本实体
那么实行流是资源吗?是的!所以不要认为一个进程能被调度,它就是进程的全部,它只是进程内部的一个实行流资源被CPU实行了!所以进程和线程之间的关系是:进程内部是包罗线程的,由于进程是承担分配体系资源的根本实体,而线程是进程内部的实行流资源!
那么怎样理解我们以前学的进程呢?其实就是操纵体系以进程为单元给我们分配资源,只是我们以进步程内部,只有一个实行流资源,也就是只有一个 task_struct!只是我们可以认为,以前我们学的进程只是进程的一种特殊情况!
[*]管理线程
那么既然操纵体系要对进程管理,假如线程多起来了,操纵体系要对线程管理吗?很显着,假如不对线程管理,那么线程就不知道自己属于哪个进程,更不知道应该实行哪个进程的代码,所以必须得对线程管理,所以需要先形貌再组织举行管理!
所以除了Linux之外,大多数操纵体系都是对线程重新举行先形貌再组织,重新为线程建立一个内核数据结构对线程管理起来,而这个结构叫做 struct tcb;除此之外还要把进程和线程之间关联起来。实际上这样做太复杂了,维护的关系太复杂了。那么 Linux 中,没有重新为线程重新计划一个内核数据结构,而是复用进程的数据结构和管理算法!
3. 进程地址空间之页表
我们上面的进程中,创建线程后给线程分配一部门代码和数据,也就是资源,那么我们应该怎样理解基于地址空间的多个实行流分配资源的情况呢?怎么知道哪部门资源给哪个线程呢?接下来我们基于地址空间理解一下。
首先CPU里面有一个CR3寄存器,它会保存页表的地址,方便找到进程的页表。我们也知道,物理内存被分为许多的页框,每个页框的大小为 4KB。下面我们理解一下虚拟地址是怎样转换为物理地址的,我们以32位的盘算机为例,也就是虚拟地址也是32位的。
接下来我们展开说一说页表。首先,页表不是一个整体,我们假设页表是一个整体,就单单是一个映射关系,如下图,每一列分别是虚拟地址、物理地址、权限,假设每一行就10个字节,单单这一个页表建立整个虚拟空间的地址映射关系就需要有 2^32 个映射条目,这样算下来这个页表就已经几十G了,所以页表不可能是这个形式的。
https://i-blog.csdnimg.cn/blog_migrate/9ce1ae31d55447090f1e32f134669ada.png
其实 32 位的虚拟地址不是一个整体,其实是将它分为了 10 + 10 + 12,此中 10 + 10 分别代表一级和二级目次。
此中第一级页表,只有 1024 个条目,也就是一个数组,由于用 10 个比特位表示的最大值就是 1024,所以这 10 个比特位代表的十进制数就是该一级页表的下标,而一级页表中存放的是二级页表的地址,所以只需要拿着前十位找到二级页表的地址,找到二级页表,然后拿着次十位,也是 10 个比特位,把它转为十进制数,然后在二级页表中索引它的下标,那么二级页表中存的是什么呢?存的是页框的起始地址!如下图:
https://i-blog.csdnimg.cn/blog_migrate/4c5234faa15d147fb79b77b27c89635a.png
其实这个一级页表就叫做页目次,我们把页目次里面的内容叫做页目次表项;把二级页表里面的内容叫做页表表项。所以我们就能通过虚拟地址的前 20 位找到物理内存中页框的起始地址。
那么剩下的 12 位呢?那么我们知道 2^12 的大小刚好就是 4096,假如取字节为单元,也就是页框的大小!所以剩下的 12 个比特位就是作为某个物理地址的页框中的偏移量!也就是说,物理地址 = 页框起始地址 + 虚拟地址的末了12位!所以这就是虚拟地址到物理地址转换的过程!
在正常情况下,我们不可能将虚拟空间全部用完,所以二级页表也不肯定全部存在。所以当需要访问一个虚拟地址时,怎么知道这个虚拟地址在不在物理内存中呢?就有可能在查页目次的时候,它的二级页表的目次根本就不存在,说明就没有被加载到内存,这个时候就是缺页中断。另外,也有可能二级页表和页框没有建立映射关系,在二级页表中尚有一个字段中的标志位会记录页框是否存在。
那么就有一个标题了,我们通过页表找到的是物理内存的某一个地址,但是对于某一个范例,可能是 int、double 等等,我们并不是访问一个字节呀,对于上面两种范例我们访问的是 4、8 个字节啊。这时候,就能体现了范例的代价!比方一个整型变量 a,占4个字节,就要有4个地址,但是为什么我们 &a,只拿到了一个地址?由于我们只能取一个地址,那么4个地址中只能取最小的那一个,由于有范例的存在,我们只要从下往上一连读取 4 个字节就能找到它了!也就是根据起始地址+偏移量读取该变量。那么CPU怎么知道根据什么范例读取多少字节呢?其实范例是给 CPU 看的,CPU在读取范例时,是知道有多少字节的!我们根据软件帮CPU找到起始地址,接下来CPU就要读取内存,读的过程把物理内存在硬件上拷贝给CPU,拷贝的时候CPU就知道拷贝多少字节了!
所以我们上面说的 CR3 寄存器中,指向的其实是页目次的地址,任何一个进程必须得有页目次。假如对物理地址举行访问的时候,假如物理地址不存在,大概越界了,CPU 中的 CR2 寄存器,保存的是引起缺页中断大概非常的虚拟地址,完成建立物理地址后就会去 CR2 取回对应的虚拟地址。
末了,我们谈上面的内容都是为了理解怎样举行资源分配的,线程的资源全部都是通过地址空间来的,而代码和数据都是通过地址空间+页表映射来的,所以线程分配资源的本质,就是分配地址空间范围!
4. 线程和进程切换
为什么线程比进程要更轻量化呢?
[*]创建和开释更加轻量化
[*]切换更加轻量化
线程切换时,线程的上下文肯定是要切换的,但是,页表不需要切换,地址空间不需要切换,所以线程在切换的时候,只是局部在切换,所以线程切换的效率更高。
https://i-blog.csdnimg.cn/blog_migrate/64fad75e8c55ba090806058dae59f302.png
线程在实行,本质就是进程在实行,由于线程是进程的实行分支。线程在实行本质就是进程在调度,CPU内有一个硬件级别的缓存,叫做 cache,cache 也是根据局部性原理,将线程/进程当前访问的代码附近的代码都加载到 cache 中,所以在进程调度的时候它应该会越跑越快,由于它的掷中率会越来越高,这部门 cache 我们称为进程运行时的热数据,热数据就是这部门数据被高频访问,所以CPU在硬件上它就会把对应的数据加载到 cache 里。所以在调度的时候,它切换的是一个进程中的多个线程,那么它在切换的时候,此时上下文固然不停在变化,但是 cache 里的数据不停不变,大概少量的更新,由于每一个线程许多属性都是共享的,就是为了让多个线程同时访问,所以数据就可以在一个进程内部的多个线程互相调度的时候,CPU当前 cache 中的数据就可以被多个线程用上,所以在线程切换的时候,只需要切换线程,不需要对 cache 保存。但是当线程的全部时间片用完了,整个进程也要被切换,CPU寄存器要保存,最重要的是,热缓存数据需要被丢弃掉,把另一个进程放上来,需要重新缓存 cache 中的数据,就要需要由冷变热,这就需要一段时间。所以线程切换的效率更高,更重要的是体现在 cache 数据不需要重新被缓存!
5. 线程的优点
[*]创建一个新线程的代价要比创建一个新进程小得多;
[*]与进程之间的切换相比,线程之间的切换需要操纵体系做的工作要少许多;
[*]线程占用的资源要比进程少许多;
[*]能充实利用多处理器的可并行数量;
[*]在等待慢速I/O操纵竣事的同时,程序可实行其他的盘算任务;
[*]盘算密集型应用,为了能在多处理器体系上运行,将盘算分解到多个线程中实现;
[*]I/O密集型应用,为了进步性能,将I/O操纵重叠。线程可以同时等待不同的I/O操纵。
6. 线程的缺点
[*]性能损失
[*]一个很少被外部事件壅闭的盘算密集型线程往往无法与共它线程共享同一个处理器。假如盘算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增长了额外的同步和调度开销,而可用的资源不变。
[*]结实性降低
[*]编写多线程需要更全面更深入的思量,在一个多线程程序里,因时间分配上的细微偏差大概因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏掩护的。
[*]缺乏访问控制
[*]进程是访问控制的根本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
[*]编程难度进步
[*]编写与调试一个多线程程序比单线程程序困难得多。
7. 线程非常
[*]单个线程假如出现除零,野指针标题导致线程崩溃,进程也会随着崩溃;
[*]线程是进程的实行分支,线程出非常,就类似进程出非常,进而触发信号机制,终止进程,进程终止,该进程内的全部线程也就随即退出。
8. 线程用途
[*]合理的利用多线程,能进步CPU密集型程序的实行效率;
[*]合理的利用多线程,能进步IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
9. 线程和进程
[*]进程是资源分配的根本单元
[*]线程是调度的根本单元
[*]线程共享进程数据,但也拥有自己的一部门数据:
[*]线程ID
[*]一组寄存器(线程上下文)
[*]栈
[*]errno
[*]信号屏蔽字
[*]调度优先级
进程的多个线程共享同一地址空间,因此代码区数据区都是共享的,假如定义一个函数,在各线程中都可以调用,假如定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和情况:
[*]文件形貌符表
[*]每种信号的处理方式(SIG_ IGN、SIG_ DFL大概自定义的信号处理函数)
[*]当前工作目次
[*]用户 id 和组 id
二、线程控制
1. pthread 线程库
由于 Linux 中没有专门为线程计划一个内核数据结构,所以内核中并没有很明确的线程的概念,而是用进程模仿的线程,只有轻量级进程的概念。这就注定了 Linux 中不会给我们直接提供线程的体系调用,只会给我们提供轻量级进程的体系调用!但是我们用户需要线程的接口,所以在用户和体系之间,Linux 开发者们给我们开发出来一个 pthread 线程库,这个库是在应用层的,它是对轻量级进程的接口举行了封装,为用户提供直接线程的接口!固然这个是第三方库,但是这个库是险些全部的 Linux 平台都是默认自带的!所以在 Linux 中编写多线程代码,需要利用第三方库 pthread 线程库!
(1)pthread_create()
接下来我们介绍 pthread 库中的第一个接口,创建一个线程:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
https://i-blog.csdnimg.cn/blog_migrate/eb2dd8931512e75406c843c324488627.png
此中第一个参数是一个输出型参数,一旦我们创建好线程,我们是需要线程 id 的,所以该参数就是把线程 id 带出来;第二个参数 attr 为线程的属性,我们不消关心,设为 nullptr 即可。
第三个参数是一个函数指针范例,也就是说我们需要传一个函数进去。当我们创建线程的时候,我们是想让实行流实行代码的一部门,那么我们就可以把该线程要实行入口函数地址传进去,线程一启动就会转而实行该指针指向的函数处!关于该函数指针的返回值和参数,都是 void*,由于 void* 可以吸取大概返回恣意指针范例,这样就可以支持泛型了。而第四个参数 arg 是一个输入型参数,当线程创建乐成,新线程回调线程函数的时候,假如需要参数,这个参数就是给线程函数传递的,也就是说该参数是给第三个参数函数指针中的参数传递的。
https://i-blog.csdnimg.cn/blog_migrate/bce325cf468080f2e5305a50db7b5d9d.png
而函数的返回值,假如我们创建乐成就返回0;假如失败会返回错误码,而没有设置 errno.
末了我们在编译的时候需要加上 -lpthread 指定库名称。
示例代码:
void* pthread_handler(void* attr)
{
while(1)
{
cout << "i am a new thread, pid: " << getpid() << endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
while(1)
{
cout << "i am main thread, pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
https://i-blog.csdnimg.cn/blog_migrate/a0e83ace9fc58459a0a8d604ecc6a3b2.png
如上图,我们以前写的代码中是不可能出现两个死循环的,但是利用创建线程之后就可以了,这就说明它们是不同的实行流。而它们的 pid 是一样的,就说明它们是同一个进程。
而我们右侧终端中,正在查看两个实行流,此中查看实行流的指令为:ps -aL,我们上面循环打印了方便观察,我们看到 pid 是一样的,但是 LWP 是什么呢?为什么会不一样呢?在 Linux 中没有具体的线程概念,只有轻量级进程的概念,所以 CPU 在调度时,不但仅只要看 pid,更重要的是每一个轻量级进程也要有自己对应的标识符,所以轻量级进程就有了 LWP (light weight process)这样的标识符,所以 CPU 是按照 LWP 来举行调度的!
但是我们假如杀掉上面恣意一个实行流的 LWP,默认整个进程都会被终止,这就是线程的结实性差的原因。
假如我们定义一个函数,大概全局变量,分别在两个实行流中实行,它们都可以读取到该函数和全局变量,如下代码:
void Print(const string& str)
{
cout<< str << endl;
}
void* pthread_handler(void* attr)
{
while(1)
{
cout << "i am a new thread, pid: " << getpid() << ", val = " << val << endl;
Print("i am new thread");
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
while(1)
{
cout << "i am main thread, pid: " << getpid() << ", val = " << val << endl;
Print("i am main thread");
val++;
sleep(1);
}
return 0;
}
有关线程的 id 的标题我们背面再谈。
(2)pthread_join()
那么创建线程后是主线程先运行还是新线程先运行呢?不确定,要看CPU先调度谁,那么肯定的是主线程是末了退出的!由于主线程退了整个进程就退出了,所以主线程要举行线程等待!假如主线程不举行线程等待,会导致类似于僵尸进程的标题!而 pthread_join() 就是举行线程等待的接口。
int pthread_join(pthread_t thread, void **retval);
https://i-blog.csdnimg.cn/blog_migrate/6ca022034a31b69c65c0d4ec1c9c581a.png
此中第一个参数,为线程的 id;第二个参数 retval 我们先不管,背面再介绍,设为 nullptr 即可。下面我们简单写一个程序:
void* pthread_handler(void* attr)
{
int cnt = 5;
while(cnt--)
{
cout << "i am a new thread, pid: " << getpid()<< endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
pthread_join(tid, nullptr);
cout << "main thread quit..." << endl;
return 0;
}
结果如下:
https://i-blog.csdnimg.cn/blog_migrate/191d56ef2ff34b341b960e8b373d1ed5.png
我们可以看到当新线程在运行的时候,主线程并没有直接运行竣事,而是举行壅闭等待!
接下来我们说一下第二个参数 retval;其实我们给线程分配的函数,它的返回值是直接写入 pthread 库中的,而 retval 也是被封装在库中,所以我们可以根据 retval 读取到函数的返回值,也就是说这个 retval 就是一个输出型参数!首先我们需要定义一个 void* 范例的变量,然后将这个变量取地址看成 pthread_join 的第二个参数传入即可!比方以下代码:
void* pthread_handler(void* attr)
{
return (void*)1234;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, pthread_handler, nullptr);
void* retval;
pthread_join(tid, &retval);
cout << "main thread quit, retval = " << (long long)retval << endl;
return 0;
}
https://i-blog.csdnimg.cn/blog_migrate/1257bd96f113832b08896f90173785b1.png
(3)pthread_exit()
那么除了在函数中直接 return 终止线程外,尚有什么方法吗?有的,pthread_exit() 接口就是用来终止线程的:
void pthread_exit(void *retval);
https://i-blog.csdnimg.cn/blog_migrate/4ef26659398c54601152395ce10ee63b.png
参数就是和 void* 返回值一样。注意线程内不能利用 exit() 体系接口终止线程,由于 exit() 是用来终止进程的!比方:
void* pthread_handler(void* attr)
{
pthread_exit((void*)1234);
}
https://i-blog.csdnimg.cn/blog_migrate/f5f2f8e595f8f75e53d28ffa1fd19384.png
(4)pthread_cancel()
除了上面的方法,pthread_cancel() 也可以取消一个线程,参数就是目标线程的 id:
int pthread_cancel(pthread_t thread);
https://i-blog.csdnimg.cn/blog_migrate/2ae1618f6ccb247a8f28e0a3255a0e36.png
返回值如下:
https://i-blog.csdnimg.cn/blog_migrate/f084c8e740128c4d46289897ce6077dd.png
假如 thread 线程被别的线程调用 pthread_ cancel 非常终掉, pthread_join 第二个参数 retval 所指向的单元里存放的是常数PTHREAD_ CANCELED,也就是 -1.
(5)简单利用 pthread 库
假设我们现在需要写一个线程举行整数相加,代码如下:
Request 类为一个需求类,_start 和 _end 为需要求的整数相加的范围。
class Request
{
public:
Request(int start, int end)
:_start(start)
,_end(end)
{}
~Request()
{
cout << "~Request()" << endl;
}
public:
int _start;
int _end;
};
Result 类为一个结果的类,Run 方法为求和方法;_result 为盘算结果;_exitcode 为记录盘算结果是否可靠。
class Result
{
public:
Result(int result, int exitcode)
:_result(result)
,_exitcode(exitcode)
{}
void Run(int start, int end)
{
for(int i = start; i <= end; i++)
{
_result += i;
}
}
~Result()
{
cout << "~Result()" << endl;
}
public:
int _result; // 计算结果
int _exitcode; // 计算结果是否可靠
};
下面为测试代码:
void* countSum(void* args)
{
Request* rq = static_cast<Request*>(args);
Result* res = new Result(0, 0);
res->Run(rq->_start, rq->_end);
return res;
}
int main()
{
Request* rq = new Request(1, 100);
pthread_t tid;
pthread_create(&tid, nullptr, countSum, rq);
void* res;
pthread_join(tid, &res);
Result* req = static_cast<Result*>(res);
cout << req->_result << endl;
delete req;
delete rq;
return 0;
}
结果如下:
https://i-blog.csdnimg.cn/blog_migrate/b1a3dd63a945e0ab5618c8ff80b47ac9.png
所以线程的参数和返回值,不但仅可以用来举行传递一样寻常参数,也可以传递对象!
2. 理解线程库
(1)线程 id
我们上面学习了 pthread_create() 接口,但是第一个参数就是线程的 id,我们至今都没有介绍过它,所以我们可以实验打印一下看一下它究竟长什么样,如下:
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, mythread, nullptr);
cout << "tid = " << tid << endl;
pthread_join(tid, nullptr);
return 0;
}
https://i-blog.csdnimg.cn/blog_migrate/eb828603af83503fc309ab5f1e3364d7.png
我们可以看到 tid 是一个非常大的数字,假设我们换成十六进制呢?如下图:
https://i-blog.csdnimg.cn/blog_migrate/5357018384c1d63af47d3ee083d0b9e6.png
我们可以看到,它很像一个地址。
假如线程想要获得自己的线程 id,还可以通过线程库中的接口获得,如下:
pthread_t pthread_self(void);
https://i-blog.csdnimg.cn/blog_migrate/4119223e265a9d5d5ea21a6a6aa71e31.png
返回值就是线程的 id.
那么这个线程 id 究竟是什么呢?由于 Linux 中没有明确的线程概念,所以没有直接提供线程的体系接口,只能给我们提供轻量级进程的体系接口,那么体系中是怎么创建轻量级进程呢?其实是用 clone() 接口,如下:
https://i-blog.csdnimg.cn/blog_migrate/a221fe811268611c0c67f0079bf25739.png
其实这个接口就是创建一个子进程,fork() 的底层原理和 clone() 类似,但是 clone() 是专门用来创建轻量级进程的。第一个参数函数指针范例,就是新创建实行流要实行的函数地址入口;第二个参数 child stack 就是自己自定义的栈;第三个参数就是是否让地址空间共享;背面的参数就不消关心了。
所以,这个接口就被线程库封装了,给我们提供的就是我们上面所介绍的线程库的接口。所以,clone() 允许用户传入一个回调函数和一个用户空间,来代表这个轻量级进程运行过程中所实行的代码,它在运行中的临时变量全部放在用户空间栈上。也就是说,线程库需要封装 clone() 的话,线程库中每一个线程都要给 clone() 提供实行方法,还要在线程库中开发空间。所以,线程的概念是库给我们维护的。另外,我们用的第三方线程库,是需要加载到内存里的!而且是加载到共享区中!那么,在 pthread 库里面,每个创建好的线程,它就要为该线程在库里面开发一段空间,用来充当新线程的栈!也就是说,新线程的栈是在共享区当中的!
那么,线程的概念是库给我们维护的,也就是说线程库要维护线程的概念,不需要维护线程的实行流。也就是,线程库中的线程在底层中对应的其实是轻量级进程的实行流,但是线程相干的属性等字段,必须需要库来维护!所以线程库注定了要维护多个线程属性的集合,所以线程库需要先形貌,再组织管理这些线程!如下图:
https://i-blog.csdnimg.cn/blog_migrate/6e13c64f9b57fe9e22eae7259c701d20.png
所以,我们每创建一个线程,在线程库中就要为我们创建线程库级别的线程,我们把它叫做线程控制块。所以这个线程控制块我们就可以理解成 tcb,那么对于每一个 tcb 在库中可以理解成用数组的方式举行管理维护。所以为了让我们快速找到在共享库中的每一个 tcb,我们把每一个 tcb 在内存中的起始地址称为线程的 tid,即线程的 id!
(2)线程栈
每一个线程在运行时,肯定要有自己独立的栈结构,由于每一个线程都要有自己的调用链,也就是说每一个线程都要有自己调用链所对应的栈帧结构。这个栈结构会保存任何一个实行流在运行过程中的全部临时变量。此中,主线程用地址空间提供的栈结构即可,而新线程则是首先在库中创建一个线程控制块,这个控制块中有包罗默认大小的空间,就是线程栈;然后库就要帮我们调用体系接口 clone() 帮我们创建实行流,最重要的是它会帮我们把线程栈传递给 clone() ,作为它的第二个参数!
https://i-blog.csdnimg.cn/blog_migrate/e623aa725d914efc4756471f2de2d4bf.png
所以,全部对应的非主线程的栈都在库中举行维护,即在共享区中维护,具体来说,是在 pthread 库中 tid 指向的线程控制块中!
我们可以写代码验证一下每一个线程都有自己独立的栈,代码链接:验证独立栈.
结果如下,test_stack 是三个线程里的临时变量,它们的地址都不一样:
https://i-blog.csdnimg.cn/blog_migrate/e3a4a9e8923f3478fd1a967f875c0954.png
同时我们也可以验证,全局变量是可以被全部线程同时看到并访问的。
其实线程和线程之间,险些没有秘密,固然它们是独立的栈,但是线程上的数据也是可以被别的线程访问到的。
(3)线程局部存储
我们知道,全局变量是可以被全部线程访问的,但是假设我们的线程想要一个私有的全局变量呢?我们可以在一个全局变量前加上 __thread,如下:
__thread int g_val = 100;
接下来我们利用上面的代码,设置这样一个全局变量,并打印它的信息出来观察:
https://i-blog.csdnimg.cn/blog_migrate/02da0b62ef78e52ed9d2229d5c897ae3.png
我们发现,每一个线程中的 g_val 的地址都是不一样的!而且对 g_val 运算的时候,它们互不干扰!所以这个 g_val 加上 __thread,就变成了线程的全局变量!其实 __thread 不是 C/C++ 提供的,而是一个编译选项。我们发现,打印出来的地址非常大,由于它是在堆栈之间的地址!它是位于线程控制块的线程局部存储区域!
注意,线程局部存储只能定义内置范例,不能定义自定义范例!
3. 分离线程
[*] 默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其举行 pthread_join 操纵,否则无法开释资源,从而造成体系泄漏。
[*] 假如不关心线程的返回值,join 是一种负担,由于主线程需要等待别的线程,这个时候,我们可以告诉体系,当线程退出时,自动开释线程资源,这就叫做线程的分离,可以用如下接口:
int pthread_detach(pthread_t thread);
https://i-blog.csdnimg.cn/blog_migrate/7ae03acc61bc041ca904d75dee4a71a3.png
此中参数就是线程的 tid.
可以是线程组内其他线程对目标线程举行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]