操作系统:多线程

一、概述

相对创建进程,创建线程比较简单,只需要复制堆栈和寄存器的内容

线程是运行在应用进程里的、比进程更小的运行单位
一个应用程序通常作为具有多个控制线程的一个进程实现。

每个线程是独立的调度对象。 一个进程可以拥有单个或多个线程,并共享内存空间。

一个进程可以拥有多个线程,而且线程之间共享以下内容

  1. 代码段,全局变量
  2. 打开的文件标识符
  3. 工作环境,包括当前目录,用户权限等

线程的运行是进程内部的执行轨迹,是进程内部执行指令的跟踪。

多线程编程优点

  1. 响应速度快:即使部分线程发生阻塞,其他线程可以继续运行
  2. 资源共享:共享所属进程的内存和资源(允许一个应用程序在同一地址空间内有多个不同活动的线程)
  3. 经济:创建和切换线程更经济
  4. 多处理器体系结构的利用(可伸缩性):多线程可以在多处理器上并行运行

二、多线程模型

有两种不同的方法来提供线程支持。
用户线程

  1. 用户线程受内核的支持
  2. 用户线程是由用户层的线程库来管理,无需内核管理
  3. 位于内核之上

内核线程

  1. 内核线程是由操作系统来直接支持和管理
  2. 由内核来进行维护和调度

用户线程和内核线程必然存在某种关系。有一对一、多对一、多对多模型,三种方法建立起来的这种关系,我们称为映射

讨论它们之间关系之前,先做以下假设

  1. 操作系统支持内核线程
  2. 把内核线程看成是一个进程
  3. 用户线程基于内核线程运行,即用户线程阻塞内核阻塞、内核线程阻塞用户线程也阻塞(被阻塞的线程会进入到等待状态)。

1.一对一模型
一对一模型映射一个用户线程到一个内核线程

优点:

  1. 可以提供并发功能,可以在多个处理器上并行执行
  2. 在一个线程执行阻塞系统调用时,可以调度另一个线程继续执行

缺点:

  1. 创建内核线程的开销会影响应用程序性能

2.多对一模型
多对一模型映射多个用户线程到一个内核线程

缺点:

  1. 一个用户线程一但被阻塞,其他用户线程也会阻塞,整个进程被阻塞
  2. 只有一个线程访问内核,不能并行运行

3.多对多模型
多对多模型多路复用多个用户线程到同样数量或更少数量的内核线程
多对多模型没有以上缺点,当一个线程执行阻塞系统调用时,内核能调度另一个线程的执行,但需要提供调度机制

4.二级模型/双层模型(混合模型)
多对多模型的变种,仍然是多对多的模型,但也允许用户线程绑定一个特定的内核线程
在这里插入图片描述

三、线程库

线程库为程序员提供创建和管理线程的API

实现线程库的两种方法

  1. 非嵌入到内核的方式
    在用户空间中提供一个没有内核支持的库,此库的所有代码和数据结构都存在于用户空间中
  2. 嵌入到内核的方式
    由操作系统直接支持的内核库,此时所有代码和数据结构都存在于内核空间中

目前使用的三种主要线程库:

  1. POSIX Pthread: 提供用户级或内核级的库
  2. Windows:内核级库,嵌入到内核的方式
  3. Java:Java 线程API通常采用宿主系统上的线程库来实现的

四、多线程问题

1.系统调用fork()和exec()

如果进程中的某一个线程调用fork(),那么新进程会复制所有线程,或者新进程只有单个进程?
答:有的Unix 系统提供两种形式的fork()系统调用,一种复制所有线程,一种仅仅复制调用了系统调用fork()的线程。

这两种形式的fork()使用取决于应用程序。如果分叉之后立刻调用exec(),那么没必要复制所有线程,因为exec()参数指定的程序会代替整个进程,这种情况下仅复制调用线程比较适合。不过如果新进程在分叉后并不调用exec(),新进程应该重复所有线程。
如调用exec(),就复制一个;如不调用exec(),就复制全部

2.线程撤销
线程撤销是在线程完成之前终止线程

需要取消的线程一般称为目标线程

目标线程的撤销有两种情况:

  1. 异步撤销(Asynchronous Cancellation):一个线程立即终止目标线程
  2. 延迟撤销(Deferred Cancellation):目标线程不断地检查它是否应终止,这允许目标线程有机会有序终止自己

目标线程的取消可能发生“要取消的线程正在更新与其他线程所共享的数据”或“已分配资源给撤销线程”,撤销会变得困难,对于异步撤销,这尤为麻烦。所以异步撤销线程可能不会释放必要的系统资源

相反对于延迟撤销,一个线程指示目标线程撤销,但仅当目标线程检查到一个标志以确定她是否应该被撤销时,撤销才会发生

对于Pthreads,调用pthread_cancle()发起线程撤销,不过这只表示一个请求,线程取消请求的实际取消的行为依赖于线程的状态

Pthreads支持三种撤销模式(状态):

  1. 关闭: 如果系统设置为禁用线程取消的话,线程取消会待定直到可以取消线程为止
  2. 延迟: 默认状态,延迟取消
  3. 异步: 异步模式,立即终止目标线程

延迟撤销只有当线程到达撤销点,才会发生撤销。
撤销点(Cancellation Point):当一个线程认定为可以安全取消时,可以安全取消的这个点称为取消点, 即pthread_testcancel() 会被调用。

3.信号处理
在Linux系统中,线程取消是通过信号来处理的。
信号是用来通知进程某个特定事件的发生,这需要操作系统提供一种内核和进程之间的通信机制

信号的接收可以是同步或异步的,这取决于信号的来源和原因

信号有以下两种:

  1. 同步信号(内部信号):进程本身事件产生的信号,如访问非法内存、除零等
  2. 异步信号(外部信号):进程之外事件产生的信号,如按CTRL+C键等

信号处理程序是用来处理发生的事件,处理过程如下:
• 由特定事件产生信号
• 这个信号传送给进程
• 信号处理程序处理相应信号

信号处理程序有默认信号处理程序用户定义的信号处理程序

事件产生信号,传送给进程需要考虑的问题?

  • 对于单线程进程,没什么问题
  • 对于多线程进程,应该传送给进程的哪个线程
    I. 发送信号到信号所适用的线程
    II. 发送信号到进程内每个线程
    III.发送信号到进程某些线程
    IV. 规定一个特定的线程以接收进程的所有信号

对于同步信号,需要传递到产生这一信号的线程,而不是进程的其他线程。对于异步信号,情况就不明显了。有的异步信号,如终止信号(例如:< control >< C >),应传递到所有线程

4.线程池
线程池(thread pool)的主要思想是在进程开始时创建一定数量的线程,并放入到池中等待工作

优点

  1. 通常用现有线程处理请求要比等待创建新的线程要快
  2. 线程池限制了在任何时候可用线程的数量,这可以防止大量创建并发线程,有助于管理

5.调度程序激活
调度程序激活:一种解决用户线程库和内核间通信的方法

之前讨论的多对多模型或双层模型可能需要这种通信,这种协调允许动态调整内核线程数量,以便保证最优性能。

许多系统在实现多对多模型或双层模型时,用户和内核线程之间设置一种中间数据结构,这种数据结构通常称为轻量级进程(LWP:LightWeight Process)

对于用户线程库,LWP表现为一种应用程序可以调度用户线程的虚拟处理器

每个LWP与内核线程相连,只有内核线程才能通过操作系统调度在物理处理器上运行。如果内核线程阻塞,LWP也阻塞,相连用户线程也会阻塞

用户线程库和内核之间的一种通信方案称为调度器激活,工作如下:内核提供一组LWP给应用程序,应用程序可调度用户线程到一个可用的LWP上

内核将特定事件通知应用程序,这个步骤叫做回调,由线程库通过回调处理程序来处理

当应用程序的线程要阻塞时,一个触发回调的事件会发生。这种情况下,内核向应用程序发出一个回调,通知他有一个线程将会阻塞并标识特定线程。然后,内核分配新的LWP给应用程序,应用程序在新的LWP上运行回调处理程序,它保存阻塞线程的状态,释放阻塞线程运行的LWP。调度另一个适合在新LWP上运行的线程。

在这里插入图片描述
在这里插入图片描述

参考

《操作系统概念》

猜你喜欢

转载自blog.csdn.net/weixin_44759105/article/details/111318412