Linux多线程(线程的创建,等待,终止,分离)
@[toc]
一、线程的概念
(1)线程的定义
在OS书籍中,线程的概念通常是这样的:
线程是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细,更加轻量化。
通过这段话我们知道,线程与进程的比是n比1的,在其他的操作系统中,由于线程的数量较多,OS需要对线程管理起来,就会存在先描述,后组织这一方式来管理线程。
但是在Linux系统下,并不是这样管理线程的。
(2)Linux下线程的概念
在Linux系统下,当在进程中创建线程时,多个线程共享一个地址空间,它没有专门为线程设计类似PCB的结构体,而是直接用PCB来模拟线程。当前进程的资源(代码+数据)被划分成若干份,让每一个PCB来使用。
在CPU的角度,此时CPU看到的PCB<=之前看到的PCB的概念,一个PCB就是一个需要被调度的执行流。它不关心执行的是一个进程的一部分(线程),还是整个进程,它只认PCB并处理PCB。
Linux这样处理线程的好处在于,不需要维护复杂的线程与进程的关系,不用单独为线程设计任何算法,直接使用进程的一套方法去处理线程的问题。OS只需要将精力放在线程之间的资源分配上即可。
(3)对进程概念的重新理解
引入线程的概念之后,我们发现,其实进程就是一堆线程的集合。一堆PCB形成了一个进程。进程内其实可以有多个执行流的。
创建进程的成本要比创建线程高,如果创建线程,只需要创建PCB并进行资源分配即可。但如果要创建进程,不仅仅要创建一个PCB还要创创建进程地址空间,页表以及完成与内存之间的映射关系。
因此,在OS的角度来看,进程是承担分配系统资源的基本实体。而线程是CPU调度的基本单位
将线程也使用PCB表示体现了系统设计者对线程的理解至深,Linux系统下的PCB的含义要<=传统意义上的进程。
Linux进程我们也称为轻量化进程。
(4)Linux下线程的接口
由于Linux线程时使用进程的PCB所模拟的,因此创建一个线程和创建一个进程的方式差不多,因此对于用户来说极其不友好。
Linux没有直接提供给我们操作线程的接口,而是给我们提供同一个地址空间创建PCB的方法以及分配资源给指定的PCB的接口。
而一些直接创建线程,释放线程,等待线程等等看起来更加实用的接口,并没有给我们提供。
不过幸运的是,大佬工程师们已经利用Linux提供的接口实现了以上这些比较实用的接口,并打包放在一个原生线程库中(用户层)。我们创建和使用线程的时候,只需要调用该库的接口即可。
比如其中创建进程的接口时pthread_create
(5)线程之间的共享资源
进程相互之间是独立的,这是因为每个进程都有一个独立的进程地址空间。而线程使用同一份进程地址空间,因此它们的大部分资源是共享的,我们只需要记住一些不被共享的资源即可:
线程ID
一组寄存器
栈
errno(错误码)
信号屏蔽字(block)
调度优先级
对于线程之间共享的资源有很多,比如各种信号的处理方式(默认,忽略或自定义),当前工作目录,文件描述符表(进程的文件),用户id和组id。同时Text Segment和Data Segment都是共享的,如果定义一个函数,在各个进程中都可以调用,如果定义一个全局变量,在各个进程中也都可以使用。
(6)线程的优点
计算密集型应用:加密,大数据运算,主要使用的是CPU资源。为了能在多处理器系统上运行,将计算分解到多个线程中去实现。
I/O密集型应用:网络下载,云盘,ssh,在线直播,看电影网络游戏等。为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。
对于计算密集型应用,并不是线程越多越好,线程太多,会导致线程之间被过度调度(有成本)。
对于I/O密集型应用,也不是线程越多越好,不过IO允许多一些线程的,在IO的场景中大部分时间是等待IO就绪的。
二、线程的创建
(1)线程的pid
可以使用上文提及的pthread_create接口来创建一个线程。
它的返回值表示的是,如果创建成功则返回0,失败则返回错误码。第一个参数是一个无符号长整型,它是一个输出型参数,输出线程的id。第二个参数表示的是线程的属性,只不过我们暂时先不需要关心,置为NULL即可。第三个参数是一个返回值为void*,参数为void*的函数,它代表线程要去执行的函数。最后一个参数是传给执行函数的参数。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* thread_run(void* args)
{
const char* id=(const char*)args;
while(1)
{
sleep(1);
printf("I am %s 线程,pid:%d\n",id,getpid());
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,NULL,thread_run,(void*)"thread1");//参数规定是void*类型,因此需要进行强转
while(1)
{
printf("I am main 线程,pid:%d\n",getpid());
sleep(1);
}
}
由于使用了第三方库,因此在编译的时候需要加上-lpthread的选项:
gcc mythread.c -o mythread -lpthread
其中主执行流(main)创建完线程之后,直接执行下方的死循环,而线程thread1则立刻执行它的函数thread_run。
但是他们都属于同一个进程,两者最终打印的pid的值是相同的,将这个pid的值的进程杀死,两个线程都会结束。
而查看线程有一个单独的命令:
ps -aL
此时我们就可以找到进程mythread的两个线程了,可以观察到两者的pid是相同的,但是LWP是不同的,其实LWP就是线程的id,其中第一个mythread线程的PID和LWP是相等的,因此它代表的是主线程。
(2)线程的id
同理我们也可以创建多个线程,可以使用pthread_self()来打印一下主线程的id,这个函数作用是打印其所在的当前线程的id:
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#define NUM 5
void* thread_run(void* args)
{
const char* id=(const char*)args;
while(1)
{
sleep(1);
printf("I am %s 线程,我的id是:%lu\n",id,pthread_self());
}
}
int main()
{
int i=0;
pthread_t tid[NUM];
for(i=0;i<NUM;i++)
{
pthread_create(&tid[i],NULL,thread_run,(void*)"thread1");
}
while(1)
{
printf("I am main 线程,我的id是:%lu\n",pthread_self());
sleep(1);
}
}
(2)线程的崩溃
直接说结论:当一个线程崩溃的时候,整个进程都会崩溃。这里不予验证了。因此线程的健壮性并不强。
线程崩溃的影响时有限的,因为线程时在进程内部,而进程是具有独立性的。
三、线程等待
一般而言,线程时需要被等待的,不等待可能出现类似僵尸进程的场景。
线程的等待函数是:pthread_join
当等待成功时,返回0,失败则返回错误码。第一个参数为被等待线程的id,第二个参数是一个输出型参数,用来获取新线程退出的时候的函数的返回值。线程函数的返回值是void*,使用二级指针来接收该返回值(void*类型)的地址,要输出时,可以解引用拿到该返回值。
当进行线程等待的时候,执行的是阻塞等待,主线程仅仅是等待不做别的内容。
#define NUM 1
void* thread_run(void* args)
{
const char* id=(const char*)args;
while(1)
{
sleep(10);
printf("I am %s 线程,我的id是:%lu\n",id,pthread_self());
break;
}
return (void*)111;
}
int main()
{
int i=0;
pthread_t tid[NUM];
for(i=0;i<NUM;i++)
{
pthread_create(&tid[i],NULL,thread_run,(void*)"thread1");
}
void* status=NULL;
pthread_join(tid[0],&status);
printf("red:%d\n",(int)status);
while(1)
{
printf("I am main 线程,我的id是:%lu\n",pthread_self());
sleep(1);
}
}
此时令新线程执行10s之后退出,并输出void*类型的退出码111,主线程在创建完新线程之后进行线程等待,并使用status来接收新线程的返回值,最终打印出来。
此时可以观察到主线程等待新线程退出后再开始执行,并且拿到了新线程的退出码111。
当新线程异常的时候,主线程不需要进行处理,因为整个进程都崩溃了。
四、线程的终止
线程的终止有三种方案:
(1)return
当主线程进行return操作,整个进程都退出。
当其他线程进行return操作,只代表当前的线程退出。
(2)pthread_exit
使用退出函数pthread_exit来退出:
它的参数为线程退出的返回值。
pthread_exit((void*)123)
此时主线程拿到的退出码就是123了。
注意,不要使用exit()函数来终止线程,因为使用该函数终止的话,整个进程都会退出。
(3)pthread_cancel
使用取消线程的函数来实现线程的退出:
传递的参数为线程的id,通常传递的是当前线程的id。当取消成功返回0,失败返回错误码。
在主线程中使用pthread_cancel取消线程:
pthread_cancel(tid[0]);
此时我们发现退出码是-1,这说明被取消的线程的退出码都是-1。我们也可以查找一下-1这个值代表的含义:
grep -ER “PTHREAD_CANCEL” /usr/include/pthread.h
此时我们可以看到,它的值是-1的。
同时我们也可以在新线程中取消主线程,此时主线程会出现defanct的失效标志。
五、线程分离
分离后的线程不需要被等待,当执行结束后会自动释放,类似信号中的显示调用子进程忽略,它会自动释放Z状态的PCB。
该函数表示,从当前进程中分离出thread号线程。此时主线程不能对其进行等待(等待失败),status也拿不到退出码,因为该线程已经与进程中的其他线程分离了。
我们可以首先让线程分离,然后再主线程中用ret接收pthread_join的返回值,如果失败则返回的不是0,同时我们也可以尝试接收一下退出码。
pthread_detach(tid[0]);
int ret=pthread_join(tid[0],&status);
printf("ret:%d\n",ret);
printf("red:%d\n",(int)status);
此时我们发现ret并不是0,并且没有接收到新线程的返回值123。说明线程已经发生了分离。
六、id与LWP
通过以上的例子我们发现,当线程运行起来之后,它的id是很长的一串,但是它在内核中的LWP并不是这个值。
这是因为我们查看到的线程id时pthread库的线程id,而不是Linux内核中的LWP,pthread库的线程id是一个虚拟地址。
在pthread库中有一个类似线程的PCB的东西,它与线程的PCB是一一对应的。
当在进程中创建多个线程的时候,每个线程都有运行时的临时数据,每个线程都有自己私有的栈结构,在进程地址空间中的栈用来存放主线程的栈。而栈与堆之间的空间中存放pthread库中的内容。在pthread库中存放有新线程的栈。id其实就是属性块的起点地址。
类似FILE中的fd,strucr pthread需要包含LWP(线程的PCB)的id,此时就建立起来的关系。
- 点赞
- 收藏
- 关注作者
评论(0)