线程
- 资源调度的最小单位
- 线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程
- 同一进程中的多条线程共享该进程中的全部系统资源,如内存空间(如代码段、数据集、堆等),文件描述符和信号处理等等。
- 同一进程中的多个线程有各自的调用栈、寄存器环境、线程本地存储等信息。
- 线程创建的开销主要是线程堆栈的建立,分配内存的开销。这些开销并不大,最大的开销发生在线程上下文切换的时候。
进程与线程
同一进程中的线程的共享资源:
- 进程代码段
- 进程的公有数据(全局变量、静态变量…)
- 进程打开的文件描述符
- 进程的当前目录
- 信号处理器/信号处理函数:对收到的信号的处理方式
- 进程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 程序可以轻松创建成百上千个协程并发执行。
参考资料