并行并发 - 2.线程

  1. 字面意思 Literal meaning
  2. 定义 Definition
  3. 层次分类 Layers
    1. 内核线程 Kernel Threads
    2. 用户线程 User Threads
      1. Pthreads
      2. Java Native Threads ( JDK >= 1.3 )
    3. 绿色线程 Green Threads (应用级)
      1. 协程 Coroutine
      2. 纤程 Fiber
  4. 线程池 Thread Pool
    1. 查看配置 Display Info
    2. 最大并行线程数 Max Parallel Threads
    3. 最佳线程数 Optimal Number Of Threads For IO-bound computation
  5. 上下文切换 Context Switch
    1. 调度方式 Scheduling
      1. 抢占模式 Preemptive
      2. 协作模式 Non-preemptive
      3. 异步抢占模式 Asynchronous preemption
  6. 扩展阅读 See Also
  7. 参考 References

字面意思 Literal meaning

StackOverflow一帖中讨论到 “线程为什么叫线程”,其中一个网友回答到:1

a line of reasoning or train of thought that connects the parts in a sequence (as of ideas or events)

再结合字典的解释 "lost the thread of the story" 2,我们可理解为,线程 是 “剧情的主线”。

本质上,同一个时间点下,一个核core 只能执行一个 线程。线程的执行就像我们看动漫或电视剧那样,如果剧情拥有多条主线,或存在主线和支线,那么电视将会播放一段时间主线后,切换到另一个主线 (支线) 。切换的操作称为 上下文切换 (Thread Context Switch)

img

定义 Definition

线程 是 轻量进程 (light-weight process) 。

下图解释了计算机中各个组件之间的关系。3 一个 Socket (插槽) 装载着一个 Processor (CPU) ,一个 CPU 包含若干个 core,一个 core 包含若干个 thread。

An image may say more than a thousand words

一个CPU 可以包含多个 线程 (threads) ,但每一个线程只能作用于同一个程序 (program) 。线程共享着 全局内存 (global memory) ,同时,每一个线程会有自己的栈 (stack) 。4

层次分类 Layers

内核线程 Kernel Threads

内核线程 (Kernel Threads) ,位于 kernel space,因此可以调用 system call。内核线程 作为一个可调度实体,归 OS 的 scheduler handles 管理。由于内核与硬件紧密相关,所以内核线程是 strongly implementation-dependent (与具体实现、硬件紧密相关联) 。

开发者一般不需要编写代码去操作 内核线程,且 内核线程 往往是由 OS 进行管理,也难以触碰。

用户线程 User Threads

用户线程 (User Threads) 的优点:

  • Pthreads 具有跨平台性,使用 Pthreads 的程序往往具有可移植性。5
  • 上下文切换的时候,不需要使用 system call,因此效率高。

Pthreads

POSIX.1 定义了一系列的用于线程编程的接口 (函数、头文件) ,该接口就是 POSIX Threads (Pthreads)4 遵循 Pthreads 标准的实现 (Implementations) 能在大多数 UNIX 系统都上运行。在 Linux,Pthreads 的实现由两种,并且均由于 GNU C library 提供4

  • LinuxThreads: glibc 2.4 之后不再支持
  • NPTL (Native POSIX Threads Library) : glibc 2.3.2 之后可使用,且 性能高,更符合 POSIX.1

且上述两者均是所谓的 1:1 实现 (implementations) ,换言之,每一个线程对应着一个 内核可调用对象 (kernel scheduling entity) 。

场景:Linux下,我们可以使用 C语言 调用 NPTL库 创建用户线程 (user threads) ,而该 用户线程 当需要使用 system call 时,会去调用 内核线程 (kernel thread) 。

Java Native Threads ( JDK >= 1.3 )

从 Java 1.3 开始,Java 使用的是 Java 自己实现的用户线程。6 即 JVM的线程 是 用户线程。(绝大数情况下)

如果 OS 不支持 内核线程,那么 JVM 将使用 (映射到) 用户线程,否则 JVM 的线程使用 (映射到) 内核线程。现在大多数系统都支持 内核线程,所以 JVM 大多是使用 (映射到) 内核线程。

大多数系统下,开发人员所编写的线程 (即用户线程) ,将会使用 (映射到) 内核线程。7 即和 Pthreads 类似。

绿色线程 Green Threads (应用级)

Green threads (virtual threads) 是由虚拟机 (如:JVM) 进行调控的,且 Green threads 位于 native threads 之上。这里所说的 native threads 是指 User Threads 和 Kernel Threads。由于 Green threads 的创建不依赖底层 OS,依赖的是 JVM,所以可以做到跨平台。

Green threads 可以说是 user-level,但是更准确地说是 application-level 的线程

Java 在 1.3 版本已经不适用 Green threads,而是使用 native threads了。 原因: (特指 Linux) Green threads 无法真正地运行在多个 CPU 上。8

Graphic

协程 Coroutine

Coroutine = co (共同) + routine (路线,指代程序) ,译成 协程。

Coroutine 将会产生若干个 非竞争 (non-preemptive) 、协调的 (cooperative) 的子程序 (subroutines) ,并通过 suspend 和 resume 的方式控制获得竞争。这种方式非常适合 cooperative tasksexceptionsevent loops,、iteratorsinfinite listspipes9

Go语言 的 Goroutines 有 Go runtime 进行管理。10 在 GO 1.5 之前,如果不设置 GOMAXPROCS,那么 Goroutines 只能运行在一个 CPU 上,在 GO 1.5 之后,Goroutines 将默认运行在所有可使用的 CPU 上。11 更多讨论可看:Golang 的 goroutine 是如何实现的?

纤程 Fiber

Fiber 与 Coroutine 的区别在于:

  • Fiber 和 thread 类似,是被 scheduler 所调用的,即被动地切换。Fiber 的 scheduler 可能是 OS的,也可能是编程语言的虚拟机 (VM)
  • 而 Coroutine 是主动,无 scheduler。

注:

  • Goroutines 属于特殊的 Coroutine ,存在 scheduler。这意味着,你可以主动管理 Goroutines ,也可以交给 Go runtime 去管理。

线程池 Thread Pool

线程池 (Thread pool) 维护着若干个线程,并提供给监视着的程序。这种设计模式可以减少线程的创建销毁带来的消耗。

查看配置 Display Info

计算机的内核可以通过以下命令查看: 1213

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ lscpu
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
Address sizes:                   39 bits physical, 48 bits virtual
CPU(s):                          6
On-line CPU(s) list:             0-5
Thread(s) per core:              1
Core(s) per socket:              6
Socket(s):                       1
NUMA node(s):                    1
Vendor ID:                       GenuineIntel
CPU family:                      6
Model:                           158
Model name:                      Intel(R) Core(TM) i5-9400 CPU @ 2.90GHz
Stepping:                        10
CPU MHz:                         1981.644
CPU max MHz:                     4100.0000
CPU min MHz:                     800.0000
BogoMIPS:                        5799.77
Virtualization:                  VT-x
L1d cache:                       192 KiB
L1i cache:                       192 KiB
L2 cache:                        1.5 MiB
L3 cache:                        9 MiB
NUMA node0 CPU(s):               0-5
Vulnerability Itlb multihit:     KVM: Mitigation: VMX disabled
Vulnerability L1tf:              Mitigation; PTE Inversion; VMX conditional cache flushes, SMT disabled
Vulnerability Mds:               Mitigation; Clear CPU buffers; SMT disabled
Vulnerability Meltdown:          Mitigation; PTI
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and seccomp
Vulnerability Spectre v1:        Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:        Mitigation; Full generic retpoline, IBPB conditional, IBRS_FW, STIBP disabled, RSB filling
Vulnerability Srbds:             Mitigation; Microcode
Vulnerability Tsx async abort:   Not affected
Flags:                           fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch
                                 _perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2
                                 apic movbe popcnt tsc_deadline_timer xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority 
                                 ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx rdseed adx smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm ida arat pln pts hwp hw
                                 p_notify hwp_act_window hwp_epp md_clear flush_l1d

笔者的计算机有一个插槽 (Socket) ,每个 Socket 有 4 个核 (Core) ,每个核可以运行1个线程 (Thread) 。下面的 CPU 是指 logical cores,数值等同于总线程数,计算方式如下: 14

1
2
3
4
CPU(s):                          6   # logical cores = 以下三个参数相乘
Thread(s) per core:              1
Core(s) per socket:              6
Socket(s):                       1

最大并行线程数 Max Parallel Threads

一个时间点下,1 个 core 只能执行 1 个线程。因此该主机的最大并行线程数 1*6=6

CPU密集型 (CPU-bound) 的线程池的理想线程数为:

1
Number of threads = core

或者

1
Number of threads = core +1

其中,core +1 的 1 是用于保险 (可能会出现什么错误导致某个线程停止使用) 15

最佳线程数 Optimal Number Of Threads For IO-bound computation

这个概念,对应着是 线程池 (Thread Pool) 的最小线程数和最大线程数,取决因素有若干。在 【原创】腾讯面试官:线程池要设置多大 中提到,线程数取决于 CPU数 以及 IO耗时。

IO密集型 (IO-bound) 的计算公式如下:16

1
 Number of threads = Number of Available Cores * (1 + Wait time / Service time)
  1. 最大并行线程数
  2. 等待时间 (IO等操作) / (线程执行任务所需要的时间 + 上下文切换)

PS: 上下文切换的时间忽略不计。

上下文切换 Context Switch

上下文切换 是指,保存某事物当前的状态 (上下文) ,通过 重新加载 (restore) 、继续执行 (resume) 等方式,将 CPU 上的事务切换到另一个事务。

调度方式 Scheduling

根据调度模式不同,上下文切换的次数也会不同,多线程开发往往会选择上下文切换次数少,且可控性高的 协作模式 (non-preemptive) 为主。如:Go语言的 Goroutine,选择

抢占模式 Preemptive

抢占模式 是一种调度方式,在 抢先模式 下,中断 (interrupt) 是由外部的调度器 (scheduler) 进行管理的,且任务间、进程间是独立的,没有任何合作 (cooperation) 。

大多数系统的调度方式都是 抢占模式。在抢占模式下,系统获得安全性——进程无法一直占用着CPU (deadlock) 。代价是,随着进程、内核线程的数量增加,切换的次数会增加。即,上下文切换时间增加

协作模式 Non-preemptive

协作模式 (Cooperative ) 又叫 非抢占模式 (non-preemptive) 。在该模式下,OS的调度器 (scheduler) 不会干预上下文切换。因此,执行单元可能一直占用CPU (不安全) 。优点:可控、上下文切换数少 (一般而言) 。用户线程、绿色线程大多选择该模式。

异步抢占模式 Asynchronous preemption

Golang 使用的 异步抢占模式 ,当运行时间超过 10 ms时候,其他 goroutine 将会尝试抢占。具体是:有一个线程叫 sysmon 监视有没有 goroutine 超时,一旦超时,将会发送一个信号 (SIGURG) 至 signal handler,signal handler 将会切换 超时的goroutine 至另外的 goroutine 。17 由于这种机制是没有堵塞 (blocking) 的,在发送信号后就结束,因此叫做 异步抢占模式。

这种模式是结合 抢占模式安全协作模式高效。抢占模式下,Context switch 的 System call 的代价过于大,而 异步抢占模式 由自定义的 handler 进行控制,因此代价相对小。

扩展阅读 See Also

参考 References


「并行并发」 系列文章
Part 1 -并行并发 - 1.并行并发、异步同步
Part 2 -并行并发 - 2.线程