Linux多线程(线程的创建,等待,终止,分离)

一、线程的概念

(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,此时就建立起来的关系。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卖寂寞的小男孩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值