Linux线程概念与控制
1、线程概念
1.1、进程地址空间回顾
进程地址空间前置知识总结:操作系统要给进程创建一个PCB——进程控制块,在Linux中是task_struct,并且要创建进程地址空间struct mm_struct,task_struct里面有个struct mm_struct* mm指针指向了虚拟地址空间,还要创建页表,然后将可执行程序的代码和数据加载到内存中,通过页表实现虚拟地址到物理地址的映射。页表也还有对应代码和数据是否加载到内存、可读可写的标志位。内存也要被操作系统管理起来,内存是以4KB大小进行管理的,我们称之为页框,可执行程序在编译好之后内部就有地址了,此时称之为逻辑地址,本质就是虚拟地址。并且操作系统对于程序的加载采用的是惰性加载的方式,先加载一部分到内存中,当进程通过某个虚拟地址查页表发现还没加载到内存就会触发缺页中断,在内存中开辟新空间将数据代码加载到内存中,然后页表建立映射。程序编译好之后是有格式的——ELF格式,有表头,表头里面存储了可执行程序的入口地址,会加载到CPU中,这样CPU就可以找到可执行程序的入口地址。CPU中有eip/pc寄存器,指向了当前执行代码的下一行代码的地址。CPU中还有个CR3寄存器存储了页表的地址,可以快速找到页表。虚拟地址到物理地址的转换还有MMU内存管理单元配合进行,一般内置在CPU中,快速进行虚拟地址向物理地址的转换。CPU读取指令执行,可以知道该条指令的大小,然后根据指令的地址加上指令的大小就可以知道下一条指令的地址,并且如果调用某个函数call一个地址,这个地址也是虚拟地址。
程序调用了库函数,如果是动态链接的方式需要加动态库加载到内存中,然后映射到虚拟地址空间的共享区,并且动态库加载的位置是不固定的。由于可执行程序编译好后地址已经确定,也就是调用的库函数已经确定,而动态库加载的时机不确定,加载的位置是随机的,所以无法将对应库函数加载到对应的位置。因此动态库的库函数采用的是相对编制的方式,通过动态库的起始地址加上可执行程序中调用库函数的地址,跳转到动态库中去执行,执行完再返回正文代码继续向后执行。并且动态库只加载一份到内存,其他进程调用库函数也是映射到自己的虚拟地址空间,所以动态库是共享库。
进程地址空间的3-4GB是内核空间,内核空间的虚拟地址可以通过减去一个固定大小的地址就可转换成物理地址,但是有些数据还是需要通过内核级页表进行映射的。内核级页表只有一份,而用户级页表有多份。当调用系统调用时,直接跳转到内核空间去执行,执行完再返回正文代码继续向后执行。所以以后进程运行的时候,执行的任何代码都是直接在自己的地址空间中进行的。但是执行系统调用的时候需要进行身份切换,CPU中有个ecs寄存器,它的低两位二进制组合有:00、01、10、11,其中11表示用户态,00表示内核态。再进入内核空间执行系统调用前,要先执行一条汇编语句:int 80,将用户态转换成内核态,然后才能执行系统调用。
Linux内核O1调度算法,有两个优先级数组,两个指针指向正在运行和等待队列,当该进程跑完或者来了个新的进程就到等待队列中去等,等调度完当前运行队列的进程,把两个指针一交换就可以继续调度。同时用位图来表示对应优先级下是否有进程。
1.2、如何理解Linux线程
首先给出操作系统对于线程的定义:线程是进程内部的一个执行分支。线程的执行粒度比进程要细。

上图,创建子进程后,实际就是以父进程task_struct为模板初始化子进程的task_struct,然后虚拟地址空间和页表给子进程拷一份,映射到相同的物理地址,文件描述符表也拷一份,父子进程都有信号对应的三张表:block、pending、handler。当父子进程某一方对数据进行写入时就发生写时拷贝触发缺页中断,开辟新空间页表建立新映射,所以父子进程不会相互影响,实现了进程间的独立性。创建子进程的过程,需要创建task_struct、虚拟地址空间、页表。进程想访问代码,想访问数据、库函数、命令函参数和环境变量、操作系统,都是需要通过进程地址空间和页表的。
所以进程地址空间是进程的资源窗口。

现在我也创建一个"进程",但是这个"进程"只创建了task_struct,并没有再创建虚拟地址空间和页表,如上图。我们给这个新的task_struct分配一些代码、数据给它,这个新的task_struct和原来的task_struct共享进程地址空间,然后这个新的task_struct代码和数据通过同一个页表映射到物理内存。当然我们不只可以创建一个,还可以创建多个。我们称这个新的"进程"为线程。线程执行的就是进程的一部分代码,所以线程的执行粒度比进程要细。
1、在Linux中,线程在进程"内部执行"指的就是线程在进程的地址空间内运行。
为什么?——因为任何执行流要执行,都要有资源,地址空间是进程的资源窗口。
2、在Linux中,线程的执行粒度比进程要细。
为什么?——因为线程执行地址空间中代码的一部分。
在CPU中有进程和线程的概念吗?
并没有,CPU中只有调度执行流的概念,不管是进程还是线程都是一个执行流。
重新定义线程,什么叫做线程?——我们认为线程是操作系统调度的基本单位。
当创建一个线程,不会给这个线程额外增加时间片,如果给它增加了,那么变相的就是整个进程的时间片增加了,所以应该将该进程的时间片分出一部分给这个线程。
那么现在如何理解以前的进程呢?
以前我们给的定义为:进程 = 内核数据结构(task_struct) +代码和数据。

重新理解进程,内核观点:进程是承担分配系统资源的基本实体。
当我们创建一个进程,操作系统会创建一个执行流,创建进程地址空间、页表,物理内存开辟空间构建映射关系。给你把所有的资源都创建好,那么我们创建线程,就是创建一个新的task_struct,这个task_struct也是资源,并且这个task_struct在进程内部,所以进程是承担分配系统资源的基本实体。
PCB也是资源,不要认为进程被调度它就是进程的所有,进程也有地址空间、页表,还有自己的代码和数据在物理内存中的空间,里面还可能会有其他执行流资源。所以什么是进程呢,上图方框圈的才是进程。
进程内部包含了线程,因为进程是承担分配系统资源的基本实体,线程是进程内部的执行流资源。
我们把最先创建出来的执行流称之为主线程,后面创建的线程称为新线程。

那么我们以前创建的进程,就是操作系统以进程为单位给我们分配资源如地址空间、页表等。只不过当前进程只有一个执行流。我们以前学的进程才是特殊情况,而一个进程有多个执行流才是常规情况。
不考虑Linux。线程要有自己指向的代码,线程也要知道自己在哪个进程,线程的状态,CPU对于线程也要调度,一个进程中可能会有多个线程,操作系统中可能会存在大量的线程,线程肯定要比进程多,那么操作系统对于这些线程要不要管理呢?要,如何管理?——先描述,再组织。所以必定要有描述线程的结构体对象struct xxx。
实现方案有两种:
1、实现描述线程控制块的结构体,struct tcb;tcb->thread control block。Windows中就是这种实现方案,另外定义线程控制块结构体,然后再实现一套数据结构和管理算法。
你敢想吗?本来进程的关系维护就已经很复杂了,现在你把线程也加进来,也就是task_struct里面还要有TCB,还要维护进程线程和线程之间的各种关系,还得实现一套数据结构和管理算法。这会多么复杂,不言而喻。
Linux程序员就想,谁规定描述必须得用新的结构来描述。我们描述进程用了task_struct,它也要被调度,也有状态优先级,有自己的上下文要被切换,那我为什么还有为线程单独创建一个数据结构呢?如果单独创建一个数据结构,还得实现一套管理算法。所以我们直接复用task_struct就好了。
2、Linux的实现方案,使用task_struct模拟线程。
线程 <= 执行流 <=进程, 我们把Linux中的执行流叫做轻量级进程。
Linux中只有轻量级进程(Light Weight Process)的概念。
1.3、进程地址空间最终讲

之前我们画的页表并不是真正的页表,我们可以算一下,假设页表一个条目10字节,以32为计算机为例,总共有4GB的空间,每个地址都映射的话就是4GB*10=40GB,这还了得,你整个内存放页表都放不下。所以实际上页表的结构并不是这样的。如上图
虚拟地址如何转成物理地址?以32为虚拟地址为例:
32被拆分成三个部分,32=10+10+12。假设一个虚拟地址为:0000 0000 0000 0001 0000 0000 0000 0101。
拆成三部分就是:0000000000 0000010000 000000000101。
第一个部分和第二个部分转换成十进制数范围是:[0,1023]。第一部分对应页目录的下标,该数组有1024个元素,也可以称为一级页表。然后每个数组里面保存的是二级页表的地址,每一项我们称为页目录表项。
通过页目录表项获取二级页表的地址,可以找到二级页表,二级页表也是大小为1024的数组,每一项我们称为页表表项,里面存储的是物理内存中页框的起始地址。
然后我们惊奇的发现最后十二位计算下来刚好是4KB,而页框的大小就是4KB,所以通过二级页表找到页框的起始地址,页框的起始地址+虚拟地址最后12位=物理地址。所以虚拟地址最后12位就是你要访问物理内存在页框中的偏移量。
现在我们可以计算一下这样需要占多少空间:假设二级页表每一项4字节,那么一个二级页表就是4KB,然后总共有1024个二级页表,1024*4=4MB,那么大概算下来就是4MB出头,但是二级页表大部分情况是不全的,一级页表必须是全的,但是里面的地址可以为空。
所以创建一个进程,依然是一个很"重"的工作。
现在如何理解线程分配资源?
进程地址空间大部分都是共享的,如果我要让线程执行一部分代码该怎么做呢?直接写几份函数,让每个线程去执行就可以了。
1.4、Linux线程周边概念
线程比进程更轻量化,为什么?

1、创建和释放更加轻量化(生死)
创建进程需要创建PCB、进程地址空间、页表,在物理内存中开辟空间建立映射,而创建一个线程只需要创建PCB,然后将进程地址空间的一部分资源分给它即可。
2、切换更加轻量化(运行)
CPU内有当前进程的上下文,对于线程来说,CPU内的某些上下文就不需要切换,比如CR3寄存器也就是页表,比如进程地址空间等,当然这只是一部分。更重要的是CPU中有Cache缓存的热数据,当进程进程切换的时候,上一个进程的cache数据会被丢弃,然后需要将当前进程的数据再由冷变热,而线程切换就不需要重新cache数据。
线程的优点:
创建一个新线程的代价要比创建一个新进程小得多。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
(最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。)
线程占用的资源要比进程少很多。
能充分利用多处理器的可并行数量。
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点:缺乏控制访问、健壮性降低、编程难度提高。
线程异常:
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
线程共享进程数据,但也拥有自己的一部分数据:
线程ID、一组寄存器(线程上下文)、栈、errno、信号屏蔽字、调度优先级。
进程的多个线程共享同一地址空间,因此代码段、数据段都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id。
2、线程控制
2.1、pthread线程库
在Linux中只有轻量级进程的概念,没有线程的概念,所以Linux并没有直接的接口来创建线程,只有创建轻量级进程的接口。

vfork创建一个子进程和父进程共享进程地址空间,这就是对应的轻量级进程,但是它和fork底层调用的创建轻量级进程都是clone。

通过参数flags决定你是要创建子进程还是创建轻量级进程。clone接口才是真正意义上的系统调用。
Linux只有创建轻量级进程接口clone,但是用户要创建线程,所以在用户和操作系统之间封装一层软件层,这层软件层将clone封装成线程创建的接口,用户就不需要再了解Linux中LWP的概念,我们把线程这个概念提出来是在用户层,而不是在内核层,我们称之为用户级线程。这个软件层就是库,这个库就是pthread库,pthread库是第三方库,所以我们编译的时候必定是要g++ -l的。
Windows系统就提供了系统级别的进程和线程的创建接口。
pthread是用户级别的线程库,Linux系统自带的原生线程库。
2.2、线程创建
使用pthread_create创建线程:

使第一个参数是输出型参数,表示线程的id,第二个参数attr可以设置线程的属性,我们默认设为nullptr即可,第三个参数start_routine表示线程要执行的回调函数,这个回调函数返回值为void*,参数为void*,所以可以传参,第四个参数arg就是要传给回调函数的参数,没有设置为nullptr。
创建成功返回0,创建失败返回错误码。
使用pthread_create创建单线程:
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
printf("my name is %s\n", name.c_str());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
while (1)
{
printf("my name is main thread\n");
sleep(1);
}
return 0;
}

可以看到如果直接g++是编不过的,需要带-l指明库名称,因为pthread是第三方库,而不需要指明头文件和库文件路径是因为它已经安装到系统的默认路径下了。
Linux下查看轻量级进程可以使用:ps -aL命令。
可以看到这两个线程都是同一个进程,它们的PID相同,而它们LWP不同。
细节一:新线程创建出来后,主线程和新线程谁先执行是不确定的。
细节二:线程创建出来不会额外分配时间片,需要瓜分进程的时间片。
下面我们使用C++11的thread库演示一下:
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
#include <thread>
int main()
{
std::thread t([](){
while (1)
{
printf("I am new thread\n");
sleep(1);
}
});
while (1)
{
printf("I am main thread\n");
sleep(1);
}
t.join();
return 0;
}

我们发现也需要带-l指明库名称,因为C++11的thread库就是对pthread原生线程库进行封装。C++11thread库对不同平台的线程创建的接口进行封装,实现了C++多线程可跨平台。
使用pthread_self获取线程id

该函数无参,返回值为线程id。
主线程创建新线程,调用pthread_self获取线程id并打印:
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
printf("my name is %s, id: %ld\n", name.c_str(),pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
while (1)
{
printf("my name is main thread, id: %ld\n", pthread_self());
sleep(1);
}
return 0;
}

可以看到主线程和新线程的线程id是不同的,是一个很大的数字。
实现toHex函数将线程tid转换为十六进制数输出:
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
std::cout << "my thread name is:" << name << ", my id is: " << toHex(pthread_self()) << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
while (1)
{
std::cout << "my thread name is: main thread, my id is: " << toHex(pthread_self()) << std::endl;
sleep(1);
}
return 0;
}

下面创建多个线程:
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
std::cout << "my thread name is: " << name << ", my id is: " << toHex(pthread_self()) << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_create(&tid1, nullptr, routine, (void*)"thread-1");
pthread_t tid2;
pthread_create(&tid2, nullptr, routine, (void*)"thread-2");
pthread_t tid3;
pthread_create(&tid3, nullptr, routine, (void*)"thread-3");
pthread_t tid4;
pthread_create(&tid4, nullptr, routine, (void*)"thread-4");
while (1)
{
std::cout << "my thread name is: main thread, my id is: " << toHex(pthread_self()) << std::endl;
sleep(1);
}
return 0;
}

我们可以发现这五个线程的id都是不同的,右侧可以看到它们同属于一个进程。
细节三:多线程执行同一个routine,所以routine函数被重入了,并且线程共享进程地址空间的代码。在routine函数中打印,打印的本质就是向显示器文件写入,而显示器文件对于多个线程来说是共享资源,所以可以看到最初运行的时候打印混乱了,这就是共享资源不加以保护就会出问题。
细节四:多线程执行打印的时候都调用了toHex函数,但是toHex函数里面的buff数组并没有出问题,这是因为每个线程都有独立的栈,调用toHex函数在自己线程栈上开辟栈帧,所以多线程互相不会影响。同时可以看到进程内的函数,线程共享。
定义一个全局变量,在一个线程中获取值打印,另一个线程中对其++操作:
#include <iostream>
#include <unistd.h>
#include <string>
#include <pthread.h>
int g_val = 100;
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine1(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
std::cout << "my thread name is: " << name << ", my id is: " << toHex(pthread_self())
<< ", g_val: " << g_val << std::endl;
g_val++;
sleep(1);
}
}
void* routine2(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
std::cout << "my thread name is: " << name << ", my id is: " << toHex(pthread_self())
<< ", g_val: " << g_val << std::endl;
sleep(1);
}
}
int main()
{
pthread_t tid1;
pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");
pthread_t tid2;
pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");
while (1)
{
std::cout << "my thread name is: main thread, my id is: " << toHex(pthread_self()) << std::endl;
sleep(1);
}
return 0;
}

细节五:全局变量被所有线程共享。
所以目前来看,代码段和数据段对于所有线程来说都是共享的。
线程异常就是整个进程异常,进程收到信号,进程终止,那么所有线程都要退出。
下面在routine1加入野指针访问继续验证:

细节六:线程一旦出异常,会导致其他线程也崩溃。
2.3、线程等待
细节七:线程创建之后,也是需要被等待回收的。
理由:1、解决类似僵尸进程的问题。2、为了知道新线程的执行结果。
使用pthread_join等待线程:

第一个参数是线程id表示要等待的线程,第二个参数是回调函数的返回值。我们创建新线程后线程执行routine回调函数,回调函数可以传参void*,也可以返回void*,这个参数retval就是作为输出型参数将返回值带出来。如果不需要返回值可以设置为nullptr。
等待成功返回0,等待失败返回错误码。
创建一个新线程循环打印3次,主线程调用pthread_join等待新线程。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
int cnt = 3;
while (cnt--)
{
std::cout << "my thread name is: " << name << ", my id is: " << toHex(pthread_self())
<< std::endl;
sleep(1);
}
return 0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
std::cout << "my thread name is: main thread, my id is: " << toHex(pthread_self()) << std::endl;
int ret = pthread_join(tid, nullptr);
if (ret != 0)
{
std::cout << "wait thread error, error string: " << strerror(errno) << std::endl;
}
sleep(2);
return 0;
}

使用pthread_join会阻塞等待线程。
那么线程名字如何进行传参的呢?

在pthread_create函数内部会调用我们传入的函数指针routine,在调用的时候将我们传的arg再传给routine函数。
那么传参只能传一个整形、一个字符串吗?并不是,如果需要我们甚至可以传一个对象:
下面演示传入一个对象,线程执行任务,然后将该对象返回,主线程在pthread_join中获取返回值,并将计算结果输出。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
class ThreadData
{
public:
ThreadData()
{}
void Init(std::string name, int a, int b)
{
_name = name;
_a = a;
_b = b;
}
~ThreadData(){}
void setTid(pthread_t tid) {_tid = tid;}
void setResult(int result){_result = result;}
pthread_t getTid() {return _tid;}
std::string getName() {return _name;}
int getA() {return _a;}
int getB() {return _b;}
int getResult() {return _result;}
private:
std::string _name;
pthread_t _tid;
int _a;
int _b;
int _result;
};
void* routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while (1)
{
printf("my name is: %s, my id is: %s\n", td->getName().c_str(), toHex(td->getTid()).c_str());
int sum = td->getA() + td->getB();
td->setResult(sum);
sleep(1);
break;
}
return td;
}
int main()
{
ThreadData* td = new ThreadData();
td->Init("thread-1", 10, 20);
pthread_t tid;
pthread_create(&tid, nullptr, routine, td);
td->setTid(tid);
ThreadData* ret;
int n = pthread_join(tid, (void**)&ret);
if (n != 0)
{
std::cout << "thread join error, error string: " << strerror(errno) << std::endl;
return 1;
}
printf("wait thread success, %d+%d=%d\n", ret->getA(), ret->getB(), ret->getResult());
return 0;
}

那么为什么还需要获取返回值呢,直接定义一个全局变量,然后新线程处理完之后写入,主线程再获取不就可以了吗?–通过pthread_join可以保证新线程一定把任务处理完成了,然后主线程再去获取结果。
如果有很多的数据需要处理,我们就可以在主线程中初始化好任务对象,然后创建多个线程,交给多个线程去处理,然后主线程等待新线程,等待结束后就可以保证后面去访问这些任务对象一定是被处理完了的。比如需要很多的数据需要排序,可以分给五个线程分别去排序,排序好之后主线程再将它们归并。
细节八:传参返回值不仅可以是数组、字符串,还可以是对象。
下面演示创建多线程执行任务,主线程调用pthread_join等待线程,然后等待完毕后输出任务结果:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
#define NUM 5
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
class ThreadData
{
public:
ThreadData()
{}
void Init(std::string name, int a, int b)
{
_name = name;
_a = a;
_b = b;
}
~ThreadData(){}
void setTid(pthread_t tid) {_tid = tid;}
void setResult(int result){_result = result;}
pthread_t getTid() {return _tid;}
std::string getName() {return _name;}
int getA() {return _a;}
int getB() {return _b;}
int getResult() {return _result;}
private:
std::string _name;
pthread_t _tid;
int _a;
int _b;
int _result;
};
void* routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while (1)
{
printf("my name is: %s, my id is: %s\n", td->getName().c_str(), toHex(td->getTid()).c_str());
int sum = td->getA() + td->getB();
td->setResult(sum);
sleep(1);
break;
}
return td;
}
int main()
{
ThreadData tds[NUM];
for (int i = 0; i < NUM; i++)
{
char buff[64];
snprintf(buff, sizeof(buff), "thread-%d", i+1);
tds[i].Init(buff, i*10, i*20);
}
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, &tds[i]);
tds[i].setTid(tid);
}
for (int i = 0; i < NUM; i++)
{
pthread_join(tds[i].getTid(), nullptr);
}
for (int i = 0; i < NUM; i++)
{
printf("tds[%d]: %d+%d=%d[%s]\n", i, tds[i].getA(), tds[i].getB(), tds[i].getResult(), toHex(tds[i].getTid()).c_str());
}
return 0;
}

那么线程的堆是共享的吗?其实线程的堆也是共享的。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
int* p;
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
int cnt = 3;
while (cnt--)
{
printf("my name is: %s, my id is: %s\n", name.c_str(), toHex(pthread_self()).c_str());
sleep(1);
}
int* p = new int(10);
return p;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
int* ret;
pthread_join(tid, (void**)&ret);
std::cout << "main thread wait thread success, return val: " << *ret << std::endl;
delete ret;
return 0;
}

新线程在返回前在堆上开辟了空间,然后将指针返回。主线程通过pthread_join获取返回值输出。所以堆实际上也是共享的。谁拿到堆空间的地址,谁就可以访问堆。
那么我们说线程有独立的栈,那么栈不是共享的吗?
看下面的代码和输出结果:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
int* p;
void* routine1(void* args)
{
std::string name = static_cast<char*>(args);
int g_val = 100;
p = &g_val;
int cnt = 3;
while (cnt--)
{
printf("my name is: %s, my id is: %s, g_val: %d\n", name.c_str(), toHex(pthread_self()).c_str(), g_val);
sleep(1);
}
return nullptr;
}
void* routine2(void* args)
{
std::string name = static_cast<char*>(args);
int cnt = 3;
while (cnt--)
{
if (p)
(*p)++;
printf("my name is: %s, my id is: %s, g_val: %d\n", name.c_str(), toHex(pthread_self()).c_str(), *p);
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");
pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}

我们定义了一个全局变量int* p,然后在线程1中将线程独立栈中定义的变量g_val的地址赋值给p,然后线程二中堆该变量进行++。最终我们发现线程二修改了线程一栈中定义的变量。
所以线程共享了代码段、数据段、堆区、共享区,哪怕就是栈,其他线程想获取也是有办法的。所以线程共享整个地址空间。
2.3、线程终止
方式一:直接return终止。
下面测试一下如果在线程中调用exit函数会怎样?
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
printf("my name is: %s, my id is: %s\n", name.c_str(), toHex(pthread_self()).c_str());
sleep(1);
exit(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
int n = pthread_join(tid, nullptr);
if (n != 0)
{
printf("wait thread error, error string: %s\n", strerror(errno));
return 1;
}
std::cout << "wati thread success.." << std::endl;
return 0;
}

我们发现并没有输出等待成功信息,所以exit会直接终止整个进程,所有线程都会终止。
方式二:使用pthread_exit终止线程

参数retval为返回值。注意:线程终止后还是需要进行等待。
下面我们创建新线程执行打印三次,然后调用pthread_exit终止线程,在主线程等待并获取返回值。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
int cnt = 0;
while (1)
{
printf("my name is: %s, my id is: %s\n", name.c_str(), toHex(pthread_self()).c_str());
sleep(1);
cnt++;
if (cnt >= 3)
pthread_exit((void*)10);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
void* ret;
int n = pthread_join(tid, &ret);
if (n != 0)
{
printf("wait thread error, error string: %s\n", strerror(errno));
return 1;
}
std::cout << "wait thread success, return val: " << (long int)(ret) << std::endl;
return 0;
}

方式三:使用pthread_cancel取消线程

参数为线程id,成功返回0,失败返回错误码。
下面演示主线程取消新线程:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
printf("my name is: %s, my id is: %s\n", name.c_str(), toHex(pthread_self()).c_str());
sleep(1);
}
return 0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
pthread_cancel(tid);
void* ret;
int n = pthread_join(tid, &ret);
if (n != 0)
{
printf("wait thread error, error string: %s\n", strerror(errno));
return 1;
}
std::cout << "wait thread succcess, return val: " << (long int)ret << ", n = " << n << std::endl;
return 0;
}

我们惊奇的发现,线程的返回值是-1,这是巧合吗?并不是。。

返回的是-1这个地址强转成void*,这是一个宏PTHREAD_CANCELED。
我们还可以在新线程中对自己取消。

一般是主线程对其他线程进行取消,但是一般情况下也不会使用这个函数,也不建议使用这个函数,因为你不清楚新线程执行的情况如何。
注意:对于线程终止的三种方法,还是需要调用pthread_join对线程进行等待。
2.4、线程分离
在线程这里等待就是阻塞地等待,不存在非阻塞轮询的方式。
那如果主线程也要做自己的事情呢?可以不等待新线程,将目标线程设置为分离状态。
线程被等待状态有:1、joined,线程需要被join,这是默认的。2、detach,线程分离,主线程不需要等待新线程。
但是不管如何,多执行流情况下,主执行流肯定是最后退出的。
使用pthread_detach对目标线程进行分离:

参数为要分离线程的id,成功返回0,失败返回错误码。
下面演示主线程分离新线程,然后对新线程join获取返回值并输出。
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <string>
#include <pthread.h>
std::string toHex(pthread_t tid)
{
char buff[1024];
snprintf(buff, sizeof(buff), "0x%lx", tid);
return buff;
}
void* routine(void* args)
{
std::string name = static_cast<char*>(args);
int cnt = 2;
while (cnt--)
{
printf("my name is: %s, my id is: %s\n", name.c_str(), toHex(pthread_self()).c_str());
sleep(1);
}
return 0;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, routine, (void*)"thread-1");
pthread_detach(tid);
sleep(2);
void* ret = nullptr;
int n = pthread_join(tid, &ret);
std::cout << "wait thread succcess, return val: " << (long int)ret << ", n = " << n << std::endl;
return 0;
}

pthread_join的返回值为22,我们说过返回0表示join成功,返回其他值说明失败。说明被分离的线程就不能再join了。
下面演示新线程自己分离自己:

线程内部可不敢进行程序替换,线程程序替换会直接将整个进程的代码和数据都换掉。
那如果我就想要进行程序替换呢?我们可以在新线程回调函数中创建子进程,然后让子进程进行进程程序替换。
3、线程ID及进程地址空间布局

首先给出结论,线程的id实际上是一个地址。

我们使用的pthread库是第三方库,需要加载到内存中,然后在地址空间的共享区建立映射。那么其他进程也可能使用pthread库,所以其他进程也会加载到进程地址空间中的共享区。
但是Linux只有轻量级进程LWP,而用户要使用线程,pthread库对系统调用进行了封装实现了线程的相关接口。那如果我想要获取线程的属性呢?难道要到操作系统内部去获取吗,当然不是,我们要实现软件层面和系统层面的解耦合,所以在pthread库中要保存线程的属性信息。pthread库加载到内存中只有一份,线程可能有很多份,对于这么多线程也要管理起来,要管理就要先描述,再组织。如何描述?——struct xxx{//线程属性},我们如果给这个结构体取个名字,那当然是tcb——thread control block线程控制块。
所以库里面要维护线程的属性集合,pthread库中对线程的描述结构体为struct pthread。那么所有线程的属性集合就都在pthread库中维护起来。
之前获取回调函数的返回值,其实就可以在struct tcb结构体里面定义一个void* ret的指针,将回调函数的返回值赋值给ret,然后join的时候就可以获取了。

如图,进程地址空间的栈是主线程的栈。我们说线程有独立的栈,线程的栈是在动态库中动态申请出来的,每个线程都会动态申请出一个固定大小的栈,这个栈就是在struct pthread结构体里面保存的。然后每个线程还线程局部存储——thread local storage。每个线程属性对象通过数组组织起来。
tid就是线程属性对象的首地址,所以为什么你pthread_join传tid呢,通过tid强转成strcut pthread*指针不就找到了这个结构体对象了。
下面来看线程局部存储:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
__thread int g_val = 100;
std::string toHex(pthread_t tid)
{
char buff[64];
snprintf(buff, sizeof buff, "0x%lx", tid);
return buff;
}
void* routine1(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
printf("my name is: %s, my id is: %s, g_val: %d, &g_val: %p\n",
name.c_str(), toHex(pthread_self()).c_str(), g_val, &g_val);
sleep(1);
}
return nullptr;
}
void* routine2(void* args)
{
std::string name = static_cast<char*>(args);
while (1)
{
printf("my name is: %s, my id is: %s, g_val: %d, &g_val: %p\n",
name.c_str(), toHex(pthread_self()).c_str(), g_val, &g_val);
g_val++;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, routine1, (void*)"thread-1");
pthread_create(&tid2, nullptr, routine2, (void*)"thread-2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}

上面的代码中我们定义了一个全局变量g_val,然后创建两个线程循环打印,线程一只打印,线程二打印+修改,我们发现线程一和线程二的g_val的值都发生了变了,它们取到的地址也是一样的。这也符合我们的预期,因为数据段线程都是共享的。
下面我们给g_val变量前面加上__thread:

我们发现这时候两个线程获取到的g_val地址是不同的,并且线程2对g_val++并不会影响到线程1。
使用__thread定义的变量会给每个线程的线程局部存储都拷贝一份,相当于每个线程私有了,但是只能是内置类型。
虽说是局部存储,但是如果其他线程能拿到某个线程局部存储的地址,那么其他线程照样可以访问到,因为线程共享整个地址空间。
4、线程封装
4.1、无参版-V1
#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <string>
#include <functional>
#include <unistd.h>
namespace ThreadModule
{
using func_t = std::function<void()>;
static int number = 1;
// 强枚举类型
enum class TSTATUS {
NEW,
RUNNING,
STOP,
};
class Thread
{
// pthread_create函数中无法回调Routine函数,Routine函数还需要传this,所以需要将Routine函数设置为static
// 但是在Routine函数内无法访问非静态成员变量func,所以可以把this作为参数传给Routine
static void* Routine(void* args)
{
Thread* t = static_cast<Thread*>(args);
t->_status = TSTATUS::RUNNING;
t->_func();
return nullptr;
}
void EnableDetach() { _joinable = false;}
public:
Thread(func_t func)
:_func(func)
,_pid(getpid())
,_joinable(true)
,_status(TSTATUS::NEW)
{
_name = "thread-" + std::to_string(number++);
}
bool Start()
{
if (_status != TSTATUS::RUNNING)
{
int n = ::pthread_create(&_tid, nullptr, Routine, this);
if (n != 0) return false;
return true;
}
return false;
}
bool Stop()
{
if (_status != TSTATUS::STOP)
{
int n = ::pthread_cancel(_tid);
if (n != 0) return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}
bool Join()
{
if (_joinable)
{
int n = ::pthread_join(_tid, nullptr);
if (n != 0) return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}
void Detach()
{
EnableDetach();
::pthread_detach(_tid);
}
bool IsJoinable() { return _joinable; }
std::string Name() { return _name; }
~Thread()
{}
private:
std::string _name; // 线程名
pthread_t _tid; // 线程id
pid_t _pid; // 进程id
bool _joinable; // 线程是否分离
func_t _func; // 回调函数
TSTATUS _status; // 线程当前状态
};
}
#endif
#include "Thread.hpp"
void count()
{
for (int i = 0; i < 10; i++)
{
std::cout << "i = " << i << std::endl;
}
}
int main()
{
ThreadModule::Thread t(count);
t.Start();
std::cout << t.Name() << "is running" << std::endl;
sleep(3);
t.Stop();
std::cout << "Stop thread : " << t.Name()<< std::endl;
sleep(1);
t.Join();
std::cout << "Join thread : " << t.Name()<< std::endl;
return 0;
}

4.2、带参版-V2
#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <string>
#include <functional>
#include <unistd.h>
namespace ThreadModule
{
// template<typename T>
// using func_t = std::function<void(T)>;
static int number = 1;
// 强枚举类型
enum class TSTATUS {
NEW,
RUNNING,
STOP,
};
template<typename T>
class Thread
{
using func_t = std::function<void(T*)>;
// pthread_create函数中无法回调Routine函数,Routine函数还需要传this,所以需要将Routine函数设置为static
// 但是在Routine函数内无法访问非静态成员变量func,所以可以把this作为参数传给Routine
static void* Routine(void* args)
{
Thread<T>* t = static_cast<Thread<T>*>(args);
t->_status = TSTATUS::RUNNING;
t->_func(t->_data);
return nullptr;
}
void EnableDetach() { _joinable = false;}
public:
Thread(func_t func, T* data)
:_func(func)
,_pid(getpid())
,_joinable(true)
,_status(TSTATUS::NEW)
,_data(data)
{
_name = "thread-" + std::to_string(number++);
}
bool Start()
{
if (_status != TSTATUS::RUNNING)
{
int n = ::pthread_create(&_tid, nullptr, Routine, this);
if (n != 0) return false;
return true;
}
return false;
}
bool Stop()
{
if (_status != TSTATUS::STOP)
{
int n = ::pthread_cancel(_tid);
if (n != 0) return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}
bool Join()
{
if (_joinable)
{
int n = ::pthread_join(_tid, nullptr);
if (n != 0) return false;
_status = TSTATUS::STOP;
return true;
}
return false;
}
void Detach()
{
EnableDetach();
::pthread_detach(_tid);
}
bool IsJoinable() { return _joinable; }
std::string Name() { return _name; }
~Thread()
{}
T* getData() {return _data;}
private:
std::string _name; // 线程名
pthread_t _tid; // 线程id
pid_t _pid; // 进程id
bool _joinable; // 线程是否分离
func_t _func; // 回调函数
TSTATUS _status; // 线程当前状态
T* _data; // 线程数据
};
}
#endif
#include "Thread.hpp"
#include <unordered_map>
#include <memory>
#include <vector>
struct ThreadData
{
int _start = 0;
int _end = 0;
int _res = 0;
ThreadData(int start, int end)
:_start(start),_end(end)
{}
};
void count(ThreadData* td)
{
int sum = 0;
for (int i = td->_start; i <= td->_end; i++)
{
sum += i;
}
td->_res = sum;
}
using thread_ptr_t = std::shared_ptr<ThreadModule::Thread<ThreadData>>;
const static int NUM = 10;
int main()
{
// 创建多线程
std::unordered_map<std::string, thread_ptr_t> threads; // 哈希表映射线程名和智能指针
std::vector<ThreadData*> v;
for (int i = 0; i < NUM; i++)
{
v.push_back(new ThreadData(i, i*100));
thread_ptr_t t = std::make_shared<ThreadModule::Thread<ThreadData>>(count, v[i]);
threads[t->Name()] = t;
}
for (const auto& e : threads)
{
e.second->Start();
}
for (const auto& e: threads)
{
e.second->Join();
}
for (const auto& e : v)
{
printf("%d+...+%d=%d\n", e->_start, e->_end, e->_res);
}
for (auto e : v)
{
delete e;
}
return 0;
}

编写代码中遇到两个比较坑的问题:
1、使用using对类模板重命名需要显示实例化。上面的代码中我们将using写在了类内,不需要显示实例化,如果将using写在类外,就得显示实例化。

2、vector扩容导致ThreadData失效。


被折叠的 条评论
为什么被折叠?



