操作系统笔记-3-线程

多线程出现

传统的进程都是单线程的程序。我们总是希望自己的程序有更高的并行性,传统进程也是有办法实现这种并行性,那就是通过子进程,但是子进程是独立的数据空间,很多时候程序的不同任务都是需要访问相同数据的,因此子进程有很大的局限性。

在这种需求场景下,多线程出现了,它弥补了子进程的缺陷,因为进程内的线程共享进程的资源,可以很容易实现数据共享。

线程概念

线程在进程内部,它共享进程的地址空间。

线程属性

线程共享执行代码,data分段和打开的文件。但是有独立寄存器、程序计数器、状态和栈。第一列是线程共享的进程属性,第二列是线程独立的属性。

img

线程和进程关系

下图是一个进程包含3个线程

img

线程状态

同传统进程一致

多线程实现

用户态多线程
  • 用户态多线程结构

    img

    1. Run-Time System:用户态实现的多线程,线程的创建、调度、销毁都是在用户态,因此运行时系统负责管理线程调度,每个进程都有一个运行时系统
    2. Thread table:线程表,记录当前进程拥有的线程
  • 用户态多线程的优点

    1. 线程切换不需要切换上下文
    2. 线程切换不需要刷新缓存
    3. 可以在不支持线程的系统中实现多线程
  • 用户态多线程的缺点

    1. 虽然实现了多线程,但是因为对系统是透明的,系统并不知道进程是多线程,因此用户态多线程并不能真正并行,只能利用单核
    2. 阻塞系统调用,当进程的一个线程调用了阻塞系统调用时,整个进程都会陷入阻塞状态。内核线程阻塞了之后,无法通知进程运行时系统线程调度器导致的进程阻塞。解决方案是jacket,将阻塞系统调用替换为非阻塞系统调用,比如同步的I/O替换为异步I/O
    3. 缺页中断也会导致进程阻塞
    4. 用户态线程因为没有时钟中断,因此不能实现轮转调度
内核态多线程
  • 内核态多线程结构

    img

    1. 内核线程由系统调度
    2. 线程表,在内核空间
  • 内核态多线程优点

    1. 可以利用多核cpu,能真正提升进程的并行
    2. 阻塞系统调用问题不会导致进程阻塞,因为内核多线程是由系统调用,系统管理着线程,因此知道线程已阻塞,就可以调度其他线程执行
    3. 缺页中断时可以切换其他线程执行
  • 内核态多线程缺点

    1. 内核线程每次切换都会陷入内核,从用户态切换到内核态,这个过程是有开销的
    2. 创建和销毁线程开销,线程复用
    3. 信号是发送给进程而不是线程
    4. 当线程创建新的进程时,新进程属性(含有线程数)问题
混合实现
  • 混合实现结构

    混合模式下,一个内核进程可以映射多个用户态线程

    img

  • 混合实现的优点

    实现了最大的灵活性

线程调度激活机制
  • 线程调度激活机制结构

    img

  • 避免阻塞

    为了避免用户态线程在调用阻塞系统调用时阻塞进程,内核在发现线程执行阻塞调用时会通过upcall通知进程,进程run-time system设置当前线程为block状态,然后调度其他线程运行;当阻塞操作调用完成会再次通知进程,run-time system可以选择立即运行或者加入就绪队列

  • 硬件中断

    如果进程对该中断不感兴趣,可能是其他进程的I/O完成,run-time system可以忽略,然后恢复线程到中断前的状态继续运行;如果进程对该中断感兴趣,比如可能是某个线程的缺页加载完成,此时运行哪个线程取决于run-time system

多线程程序设计的挑战

识别任务

识别出可以独立、并发的任务,可以独立运行在多核处理器上

平衡

考虑多核运行是否值得, 根据Amdahl定律,一个应用通过增加cpu可以获得的加速,S代表任务必须串行执行占比,N代表核心数

img

一个例子,横坐标是核心数,纵坐标是加速倍数,曲线代表不同必须串行执行占比在不同核心数下的加速

img

数据划分

如果应用划分为独立的任务,那么任务访问和操作的数据必须划分到不同的cpu上

数据依赖

如果并行的任务访问的数据之间有依赖,需要同步机制来保证数据一致性,在后续的笔记中会详细介绍线程同步

测试和调试

当应用是由多线程实现时,测试和调试都比较困难

多线程应用举例

Java的多线程模型

了解完多线程后,很好奇Java的多线程模型是怎么实现的呢?JVM中并没有说明Java的多线程是用户态实现还是内核态实现。网上查了一番资料后,感觉知乎的R大的回答比较靠谱一点,链接。在较新的Hotspot VM,在除了Solaris的平台上实现的是1:1的模型,也就是内核态实现多线程。感兴趣的可以点击上面的链接查看详情。

Go语言的goroutine

Go语言中的goroutine就是一种用户态多线程,它有自己实现的调度器。

总结

多线程在提升性能的同时也给编程带来了复杂性。不过在开发过程中,不管是用什么语言,都会经过一些封装,使其可用性更高。比如Java的JUC下的线程池,它已经封装了很多细节,让我们只需要关注任务和数据的拆分,而不用关注多线程实现的细节。

参考
  1. Modern Operating Systems》第4版
  2. Operating System Concepts》第10版
  3. JVM中的线程模型是用户级的么?
  4. Golang 的 goroutine 是如何实现的?