关于线程(概念| 优缺点| 创建| 异常| 操作)

Linux 多线程

  • 背景
  • 线程的概念
  • 线程的优缺点
    • 优点
    • 缺点
  • 线程创建
    • LWP(系统角度)
    • 线程ID(用户角度)
  • 线程异常
  • 线程用途
  • 进程与线程
  • 线程等待
  • 线程终止
  • 线程分离

背景

在创建一个进程时,操作系统做了以下的工作:
1、创建一个进程所需要的各种数据结构(pcb、地址空间、页表、与文件的关系,父子关系等等)
2、开辟物理内存,把硬盘上的代码和数据加载到物理内存中。
3、把虚拟地址空间和物理内存通过页表建立映射关系。

所以,创建一个进程就是一个执行流从无到有的过程。在这个过程中,我们申请了很多资源,创建了很多东西。

当CPU调度时,要找到对应进程的代码和数据。

如果再想创建一个进程,重复上述过程。


线程的概念

关于线程(概念| 优缺点| 创建| 异常| 操作)_第1张图片

进程的概念:是承担分配系统资源的基本实体。(从操作系统观点),为了承担系统资源,操作系统为进程创建了大批数据结构和大批的内存块,以承载该进程的代码和数据。

线程:是调度的基本单位,线程是进程内部的执行流!

什么叫做线程在进程内部?

  • 线程在进程内部运行,本质是在进程地址空间内运行。 所有的线程用的是同一个进程的地址空间。

进程和线程的对应关系应该是一对多,一个进程内有多个执行流(线程)。

站在线程的角度,怎么理解之前的单进程?

  • 内部只有一个执行流的进程。

现在应该如何看待task_struct呢?

  • 之前我们认为是进程控制块,现在更新了一下视角:在Linux中,没有真正意义上的线程(指的是Linux在设计线程时没有专门为它设计数据结构),线程是用进程模拟的。
    【原因是,站在Linux角度,进程与线程本质都是要处理事物,具有相同属性,因此不必设计两种结构。】
  • 这个模拟性体现在,在Linux中,依旧使用task_struct来表示一个线程。在地址空间中,如果只有一个task_struct,认为它是一个具有单执行流的进程;如果有多个时,认为它是具有多个执行流的进程。
  • 之前认为task_struct背后挂靠的都是一个地址空间、一套页表和代码和数据;现在在CPU看来,task_struct背后有可能挂靠的是和别的task_struct共享的地址空间(也就是说这时,它只会使用地址空间的一部分)。

从CPU角度,现在它看到的task_struct都要比传统的进程更加轻量化,因此,线程也叫作轻量级进程。

线程的优缺点

优点

1、创建一个新线程的代价要比创建一个新进程小得多
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3、线程占用的资源要比进程少很多
4、能充分利用多处理器的可并行数量
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

缺点

1、性能损失
2、健壮性降低
3、缺乏访问控制
4、编程难度提高

线程创建

线程的管理依靠的是pthread库。

创建一个线程,应用的是函数pthread_create

#include 

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
void *(*start_routine)(void*), void *arg);

参数:
thread:返回线程ID(操作系统不知道这个id,是由库提供的)
attr:设置线程的属性,一般设置为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数

  • 创建一个线程

关于线程(概念| 优缺点| 创建| 异常| 操作)_第2张图片
此时现阶段有两个执行流。如何证明?

  • 一份代码有两个死循环,所以它们应该同时运行。关于线程(概念| 优缺点| 创建| 异常| 操作)_第3张图片

且生成的可执行程序是动态链接的。
关于线程(概念| 优缺点| 创建| 异常| 操作)_第4张图片
./mythread 是一个进程,但有两个执行流,如何证明这两个执行流来自一个进程?

  • ps -aL查看轻量级进程,发现它们的PID是一样的。
    在这里插入图片描述

LWP(系统角度)

在上图,我们发现有一个重要标识LWP (light weight process,轻量级进程):对进程的标识作用,调度时用的是LWP。

我们知道Linux中没有真正意义上的线程,在对线程的管理是依靠pthread库来完成的。要管理就要描述和组织线程,需要有描述线程的结构体,在用户区创建一个线程的实体,但在操作系统中需要有一个执行流跟它关联起来,也就是LWP。

线程实体与LWP是有对应关系的。可以简单理解为用户区线程实体结构体中包含对应的轻量级进程ID。
不同的操作系统对应关系是不一样的,在Linux中是一一对应的。

总结为,操作系统底层为我们提供线程的执行功能;而用户级别提供用户空间的线程管理功能。

函数pthread_self获取当前线程的线程id:
在这里插入图片描述

线程ID(用户角度)

这个ID绝不是前面提的轻量级进程ID! LWP是系统层面的,而该ID是用户层面,也就是库提供的。
pthread_create函数的第一个参数,指向的是一个虚拟的内存单元,该内存单元的地址即为新创建线程的线程ID。

也就说,动态库要被加载到共享区中,库里一定会包含描述线程的结构体,把这些结构体按照数组的形式组织在一起。线程ID可以理解成该线程在库当中,描述它自身结构的结构体的内存位置的起始地址。 这也就解释的通,为什么上图中打印出来的ID这么像地址!
关于线程(概念| 优缺点| 创建| 异常| 操作)_第5张图片

线程异常

创建一个进程,此时该进程只有一个执行流,pthread_create执行完毕后,创建了新的线程,此时有了两个执行流。

新线程执行时如果出现除零错误,操作系统识别到硬件异常,确定是哪个进程让我的CPU执行除零,向对应进程发送信号【注意:信号发送的基本单位是进程!】,然后进程的资源就被释放掉了,此时在进程中滋生的一堆线程也要使用这些资源,但资源已被释放,所以这些线程也就随之崩溃了。

因此,线程健壮性不强,只要众多中有一个出现崩溃,整个都会崩溃。

线程用途

1、合理的使用多线程,能提高CPU密集型程序的执行效率。
2、合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

进程与线程

进程与线程相比:

进程更强调独立性,但又不是绝对独立,比如fork创建子进程,进程间通信等等,都会出现资源共享的情况。

线程更强调共享,但又不是绝对共享,还有部分是自己私有的:线程ID、一组寄存器、errno、信号屏蔽字、调度优先级。

其中,要特别注意:线程私有线程运行栈!每个线程都有自己的运行栈。如果栈是共享的话,栈的FILO特性会让整个数据变得混乱;每个线程都有一套自己的寄存器!(也叫硬件上下文/任务状态段tss)这个证明线程是会被切换的。

各线程会共享进程资源和环境:文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id。
关于线程(概念| 优缺点| 创建| 异常| 操作)_第6张图片

线程等待

新线程必须被主线程等待,如果不等待就会造成类似于僵尸进程这样的问题,导致内存泄漏。

默认都是以阻塞方式等待。

关于线程的等待,我们并不用关心异常的情况,因为默认是不会错的,一旦出现错误,就是进程要去处理的。会给进程发信号,所以信号是专门为进程设计的。

int pthread_join(pthread_t thread, void **value_ptr);

thread:线程ID
value_ptr:它指向一个指针,该指针指向被等待线程的返回值。
返回值:成功返回0;失败返回错误码

#include
#include
#include
#include
#include
void* thread_run(void* arg)
{
     
	printf("%s, %lu, pid:%d \n",(char*)arg, pthread_self(), getpid());
    sleep(5);
    return (void*)10;
}

int main()
{
     
   pthread_t tid;
   pthread_create(&tid, NULL, thread_run, "thread 1");

   printf("main : %lu, pid:%d\n",pthread_self(), getpid());
   
   void* ret = NULL;
   pthread_join(tid, &ret);
   printf("thread quit code: %d\n",(long long)ret);
}

关于线程(概念| 优缺点| 创建| 异常| 操作)_第7张图片

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
    注意:exit是终止进程,pthread_exit终止线程。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
void pthread_exit(void *value_ptr);

value_ptr:value_ptr不要指向一个局部变量。因为当其它线程得到这个返回指针时线程函数已经退出了。

返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

int pthread_cancel(pthread_t thread);

thread:线程ID

返回值:成功返回0;失败返回错误码

线程分离

int pthread_detach(pthread_t thread);

如果不关心线程的返回值时,可以对线程分离。告诉系统,当线程退出时,自动释放线程资源。

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。


你可能感兴趣的:(多线程,操作系统,linux)