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

目录

一、线程的概念

(1)线程的定义

(2)Linux下线程的概念

(3)对进程概念的重新理解

(4)Linux下线程的接口

(5)线程之间的共享资源

(6)线程的优点

二、线程的创建

(1)线程的pid

(2)线程的id

(3)线程的崩溃

三、线程等待

四、线程的终止

(1)return

(2)pthread_exit

(3)pthread_cancel

五、线程分离

六、id与LWP


一、线程的概念

(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并进行资源分配即可。但如果要创建进程,不仅仅要创建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);
    }
    return 0;
}

由于使用了第三方库,因此在编译的时候需要加上-lpthread的选项:

gcc -o $@ $^ -lpthread

其中主执行流(main)创建完线程之后,直接执行下方的死循环,而线程thread1则立刻执行它的函数thread_run。

但是他们都属于同一个进程,两者最终打印的pid的值是相同的,将这个pid的值的进程杀死,两个线程都会结束。

而查看线程有一个单独的命令:

ps -aL

此时我们就可以找到进程test的两个线程了,可以观察到两者的pid是相同的,但是LWP是不同的,其实LWP就是线程的id,其中第一个test线程的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);    
  }    
}  

(3)线程的崩溃

直接说结论:当一个线程崩溃的时候,整个进程都会崩溃。这里不验证了。因为线程的健壮性并不强。

线程崩溃的影响是有限的,因为线程是在进程内部,而进程是具有独立性的。

三、线程等待

一般而言,线程是需要被等待的,不等待可能会出现类似僵尸进程的场景。

线程的等待函数是: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,此时就建立起来的关系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值