0%

进程、线程与协程

  • 进程
  • 线程
  • 轻量化线程
  • 协程

    进程

  • 资源管理的最小单位
  • windows 中每一个 exe 程序都是一个进程

线程

  • 资源调度的最小单位
  • 线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程
  • 同一进程中的多条线程共享该进程中的全部系统资源,如内存空间(如代码段数据集等),文件描述符和信号处理等等。
  • 同一进程中的多个线程有各自的调用寄存器环境线程本地存储等信息。
  • 线程创建的开销主要是线程堆栈的建立,分配内存的开销。这些开销并不大,最大的开销发生在线程上下文切换的时候。

进程与线程

同一进程中的线程的共享资源:

  • 进程代码段
  • 进程的公有数据(全局变量、静态变量…)
  • 进程打开的文件描述符
  • 进程的当前目录
  • 信号处理器/信号处理函数:对收到的信号的处理方式
  • 进程ID与进程组ID

线程独占的资源:

  • 线程ID
  • 一组寄存器的值
  • 线程自身的堆是共享的
  • 错误返回码:线程可能会产生不同的错误返回码,一个线程的错误返回码不应该被其它线程修改;
  • 信号掩码 /信号屏蔽字(Signal mask):表示是否屏蔽/阻塞相应的信号(SIGKILL,SIGSTOP除外)

线程模型

内核级线程 ( 1:1 )

内核线程的建立和销毁都是由操作系统负责、通过系统调用完成的,内核维护进程及线程的上下文信息以及线程切换。

特点:

  • 内核级线级能参与全局的多核处理器资源分配,充分利用多核 CPU 优势。
  • 每个内核线程都可被内核调度,因为线程的创建、撤销和切换都是对内核管理的。
  • 一个内核线程阻塞与他同属一个进程的线程仍然能继续运行。

缺点:

  • 内核级线程调度开销较大。调度内核线程的代价可能和调度进程差不多昂贵,代价要比用户级线程大很多。
  • 线程表是存放在操作系统固定的表格空间或者堆栈空间里,所以内核级线程的数量有限

用户级线程 ( N:1 )

实现在用户空间的线程称为用户级线程。用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全由用户空间的库函数完成,不需要内核的参与,因此这种线程的系统资源消耗非常低,且非常的高效

特点:

  • 用户线级线程只能参与竞争该进程的处理器资源,不能参与全局处理器资源的竞争。
  • 用户级线程切换都在用户空间进行,开销极低
  • 用户级线程调度器在用户空间的线程库实现,内核的调度对象是进程本身,内核并不知道用户线程的存在

缺点:

  • 如果触发了引起阻塞的系统调用的调用,会立即阻塞该线程所属的整个进程。
  • 系统只看到进程看不到用户线程,所以只有一个处理器内核会被分配给该进程 ,也就不能发挥多核 CPU 的优势

M:N 模型

多对多模型结合了一对一模型和多对一模型的优点,将多个用户线程映射到多个内核线程上。由线程库负责在可用的可调度实体上调度用户线程,这使得线程的上下文切换非常快,因为它避免了系统调用。但是增加了复杂性和优先级倒置的可能性,以及在用户态调度程序和内核调度程序之间没有广泛(且高昂)协调的次优调度。

优点:

  • 一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行
  • 多对多模型对用户线程的数量没有限制
  • 在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高

轻量级进程 (一对一模型)

Linux 并没有为线程准备特定的数据结构,因为 Linux只有task_struct这一种描述进程的结构体。在内核看来只有进程而没有线程,线程调度时也是当做进程来调度的。Linux所谓的线程其实是与其他进程共享资源的轻量级进程

轻量级线程 Light-weight Process (LWP),只有一个最小的执行上下文和调度程序所需的统计信息,它只带有进程执行相关的信息,与父进程共享进程地址空间 。

LWP 是一种由内核支持的用户线程,每一个轻量级进程都与一个特定的内核线程关联(一对一)。

由于轻量轻量级进程基于内核线程实现,因此它的特点和缺点和内核线程一致。

至于 JVM 中的线程模型是哪一种,JVM 规范里并没有定义,不过大多数实现都使用了 1:1 线程模型

协程

协程 Coroutines 是一种比线程更加轻量级的微线程。类比一个进程可以拥有多个线程,一个线程也可以拥有多个协程,因此协程又称微线程和纤程。
可以粗略的把协程理解成子程序调用,每个子程序都可以在一个单独的协程内执行。

为什么使用协程

当前无数的 Web 服务和互联网服务,本质上大部分都是 IO 密集型服务,处理的任务大多是和网络连接或读写相关的高耗时任务,高耗时是相对 CPU 计算逻辑处理型任务来说,两者的处理时间差距不是一个数量级的。
IO 密集型服务的瓶颈不在 CPU 处理速度,而在于尽可能快速的完成高并发、多连接下的数据读写。

两种传统的解决方案:

  • 如果用多线程,高并发场景的大量 IO 等待会导致多线程被频繁挂起和切换,非常消耗系统资源,同时多线程访问共享资源存在竞争问题。
  • 如果用多进程,不仅存在频繁调度切换问题,同时还会存在每个进程资源不共享的问题,需要额外引入进程间通信机制来解决。

调度开销

协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作用户空间栈,完全没有内核切换的开销。

动态协程栈

协程拥有自己的寄存器上下文和栈,协程调度切换时将寄存器上下文和栈保存下来,在切回来的时候,恢复先前保存的寄存器的上下文和栈。

Goroutine 是 Golang 的协程实现。Goroutine 的栈只有 2KB 大小,而且是动态伸缩的,可以按需调整大小,最大可达 1G 。

线程也都有一个固定大小的内存块来做栈,一般会是 2MB 大小,线程栈会用来存储线程上下文信息。2MB 的线程栈和协程栈相比大了很多。

Go 语言协程实现

Golang 在语言层面实现了对协程的支持,Goroutine 是协程在 Go 语言中的实现, 在 Go 语言中每一个并发的执行单元叫作一个 Goroutine ,Go 程序可以轻松创建成百上千个协程并发执行。

Go语言对协程的具体实现

参考资料