操作系统之大话线程与协程

2021年3月5日 518点热度 1人点赞 1条评论

线程

进程的一种理解是一个可以实现计算的虚拟机框架,执行的活跃元素由执行引擎提供。在传统操作系统中仅允许进程包含一个执行引擎。而在现代操作系统中,进程可以包含多个执行引擎。每个执行引擎称作线程。现在让我们了解一下线程。

狗狗镇楼

为什么要引入线程

在操作系统中引入进程的目的是使多道程序能并发执行,以改善资源利用率及提高系统吞吐量;

在操作系统中再引入线程,则是为了减少程序并发执行所付出的时空开销(诸如创建、撤销、切换等一系列操作所花费的时空开销),使操作系统具有更好的并发性。

线程的定义

线程的定义也有很多不同的提法,下面列出几种仅供参考:

  • 线程是进程内的一个执行单元
  • 线程是进程内的一个可调度实体
  • 线程是程序(或进程)中相对独立的一个控制流序列
  • 线程是执行的上下文,其含义是执行的现场数据和其他调度所需的信息(这种观点来自Linux系统)
  • 线程是进程内一个相对独立的、可调度的执行单元。

线程的组成

线程一般由以下几部分组成:

  • 线程数据,为线程所私有,它通常分配在特定的线程栈上,每个线程有自己私有的数据空间。
  • 线程状态,也就是保持线程所有属性的操作系统数据结构。例如,状态包括了线程将要执行的下一条指令的地址、以及线程是否在等待资源而处于阻塞状态,以及它正在等候哪一个资源的信息等。

线程的特点

  • 多线程之间可以共享同一块地址空间和所有可用数据,这是进程所不具备的。
  • 线程比进程更轻量级,所以他们比进程更容易(即更快)创建,也更容易撤销。
  • 使用线程可以提高性能。若多个线程都是CPU密集型的,那么并不能获得性能上的增强,但是如果存在着大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠进行,从而会加快应用程序执行的速度。

线程的控制

  • 和进程类似,线程也有运行、就绪、阻塞等状态。

    1. 创建:当创建一个新进程时,也为该进程创建了一个线程。线程还可以创建新线程。
    2. 就绪:线程已获得除处理机外的所有资源。
    3. 运行:线程正在处理机上执行。
    4. 阻塞:线程因等待某事件而暂停运行。
      终止:一个线程已完成。
  • 线程的同步与通信与进程类似。进程的挂起及终止将影响到其中的所有线程。

  • 进程中的线程具有:

    1. 执行状态
    2. 线程上下文
    3. 执行栈
    4. 线程静态存储局部变量
    5. 寄存器及对所属进程资源的访问

线程的实现

操作系统中有多种方式实现对线程的支持:内核级线程(kernel-level thread)用户级线程(user-level thread)以及上述两种方法的组合实现

内核级线程

  • 内核级线程是指依赖于内核,由操作系统内核完成创建和撤消工作的线程。
  • 在支持内核级线程的OS中,内核维护进程和线程的上下文信息并完成线程切换。
  • 一个内核级线程阻塞时不会影响其他线程的运行。
  • 处理机时间分配的对象是线程,所以有多个线程的进程将获得更多处理机时间。

用户级线程

  • 用户级线程是指不依赖于操作系统核心,由应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制的线程。
  • 用户级线程的维护由应用进程完成,可以用于不支持内核级线程的操作系统
  • 当一个线程阻塞时,整个进程都必须等待,处理机时间是分配给进程的,进程内有多个线程时,每个线程的执行时间相对少一些。

两种方法的组合

在有些系统中,提供了上述两种方法的组合实现。在这种系统中,内核支持多线程的建立、调度与管理;同时,系统中又提供使用线程库的便利,允许用户应用程序建立、调度和管理用户级的线程。因此可以很好地将内核级线程和用户级线程的优点结合起来。由此产生了不同的多线程模型。

  • 一对一模型
    每个用户级线程映射到一个内核级线程上:

  • 多对一模型
    多个用户级线程映射到一个内核级线程上:

  • 多对多模型
    多个用户级线程映射到较少或相等个数的内核级线程上:

协程

协程方面的内容部分参考了博客:https://www.cnblogs.com/Survivalist/p/11527949.html

协程(Coroutines),是一种基于线程之上,但又比线程更加轻量级的存在。协程可以理解为是运行在线程上的代码块,由程序员自己写程序来管理,具有对内核来说不可见的特性。协程提供的挂起操作会使协程暂停执行,而不会导致线程阻塞。

为什么提出协程的概念

在操作系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态(CPU闲置),如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底。

最常见的例子就是JDBC(它是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的ContextSwitch开销。

对于上述问题,现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是node.js以及Java里的新秀Vert.x。

而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。

协程的特点

  • 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此协程的切换在用户态就可以完成,减少了上下文切换的代价,提高了效率。
  • 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
  • 由于在同一个线程上,因此可以避免竞争关系而使用锁。协程是一个纯用户态的并发机制,同一时刻只会有一个协程在运行,其他协程挂起等待。
  • 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,最好使用线程去解决。

协程的原理

当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻 yield掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。比如Golang里的go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。

由于协程的暂停完全由程序控制,发生在用户态上;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。因此,协程的开销远远小于线程的开销,也就没有了ContextSwitch上的开销。

协程与线程的比较

比较项 线程 协程
占用资源 初始单位为1MB,固定不可变 初始一般为 2KB,可随需要而增大
调度所属 由 OS 的内核完成 由用户完成
切换开销 涉及模式切换(从用户态切换到内核态)、16个寄存器、PC、SP...等寄存器的刷新等 只有三个寄存器的值修改 - PC / SP / DX
性能问题 资源占用太高,频繁创建销毁会带来严重的性能问题 资源占用小,不会带来严重的性能问题
数据同步 需要用锁等机制确保数据的一直性和可见性 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

agedcat_xuanzai

这个人很懒,什么都没留下

文章评论

  • 乐乐

    图片是我家狗狗哟嘻嘻(♡˙︶˙♡)

    2021年3月5日