Go并发编程-线程模型
一、前言:3种线程模型
线程的并发执行是有操作系统来进行调度的,操作系统一般都在内核提供对线程的支持。
我们在使用高级语言编写程序时候创建的线程是用户线程,
根据用户线程与内核线程关系,可以划分为3种线程模型
二、3线程模型—1对1模型
- 用户线程与内核线程是1对1
- 当从程序入口点(比如main函数)启动后,操作系统就创建了一个内核进程和用户线程
2.1、优点
- 在多处理器上,多个线程可以真正实现并行运行
- 当一个线程由于网络IO等原因被阻塞时候,其他的线程不受影响。
2.2、缺点
- 一般操作系统会限制内核线程的个数,所以用户线程的个数会受到限制。
- 由于用户线程与内存线程1对1,当用户线程比如执行Io操作(执行系统调用)时候,需要从用户态的用户程序执行切换到内核态执行内核操作,然后等执行完毕后又会从内核态切换到用户态执行用户程序,而这个切换操作开销是相对比较大的。
扩展
Java的线程模型就是使用的这种1对1的模型,所以Java中多线程对共享变量使用锁同步时候会导致获取锁失败的线程进行上下文切换,而JUC包提供的无锁CAS操作则不会产生上下文切换。
三、3线程模型—n对1模型
- n个用户线程对应1个内核线程,同时同一个用户线程只能对应一个内核线程
- 对应同一个内核线程的n个用户线程的上下文切换是由用户态的运行时线程库来做的,而不是由操作系统调度系统来做的
3.1、优点
- 上下文切换在用户态,切换速度很快,开销很小;
- 可创建的用户线程的数量可以很多,只受内存大小限制。
3.2、缺点
- 由于多个用户线程对应一个内核线程,当该内核线程对应的一个用户线程被阻塞挂起时候,该内核线程对应的其他用户线程也不能运行了,因为这时候内核线程已经被阻塞挂起了。
- 这种模型并不能很好的利用多核CPU进行并发运行。
四、3线程模型—n对m模型
结合1对1和n对1模型的特点,让大量的用户线程对应少数几个内核线程上
4.1、优点
-
可以充分利用多核CPU提升运行效能
- 每个内核线程对应多个用户线程,每个用户线程有可以对应多个内核线程,当一个用户线程阻塞后,其对应的当前的内核线程会被阻塞,但是被阻塞的内核线程对应的其他用户线程可以切换到其他的内核线程上继续运行,所以n对m模型是可以充分利用多核CPU提升运行效能的。
-
只要内存够用可以无限创建用户线程
五、Go线程模型
Go线程模型属于多对多线程模型,其模型如下
Go中使用使用go语句创建的goroutine可以认为是轻量级的用户线程
5.1、go线程模型包含三个概念
- 内核线程(M)
- goroutine(G)
- 逻辑处理器(P)
5.2、go线程模型包含三个概念之间的关系
- 在Go中每个逻辑处理器§会绑定到某一个内核线程(M)上
- 每个逻辑处理器(P)内有一个本地队列,用来存放go运行时分配的goroutine。
- 在上面介绍的n对m模型中是操作系统调度线程在物理CPU上运行,在Go中则是Go的运行时调度goroutine在逻辑处理器(P)上运行。
5.3、go中存在两级调度
- 操作系统的调度系统,该调度系统调度逻辑处理器占用cpu时间片运行
- go的运行时调度系统,该调度系统调度某个goroutine在逻辑处理上运行。
5.4、go语句创建一个goroutine的过程
- 使用go语句创建一个goroutine
- 创建的goroutine会被放入go运行时调度器的全局运行队列中
- go运行时调度器会把全局队列中的goroutine分配给不同的逻辑处理器(P),分配的goroutine会被放到逻辑处理器(P)的本地队列中
- 当本地队列中某个goroutine就绪后待分配到时间片后就可以在逻辑处理器上运行了,如上图goroutine1当前正在占用逻辑处理器1运行。
5.5、go避免某些goroutine出现饥饿现象
- 为了避免某些goroutine出现饥饿现象,被分配到某一个逻辑处理器(P)上的多个goroutine是分时在该逻辑处理器运行的,而不是独占运行直到结束。
- 举例:假设每个goroutine从开始到运行结束需要3分钟,那么当前逻辑处理器下的goroutine1,goroutine2,goroutine3运行情况如下
- 0-1分钟:goroutine1
- 1-4分钟:goroutine2
- 4-6分钟:goroutine1
- 6-9分钟:goroutine3
- 举例:假设每个goroutine从开始到运行结束需要3分钟,那么当前逻辑处理器下的goroutine1,goroutine2,goroutine3运行情况如下
5.6、goroutine阻塞和迁移
goroutine内部实现与在多个操作系统线程(Os 线程)之间复用的协程(coroutines)一样。
如果一个goroutine阻塞OS线程,例如等待输入,则该OS线程对应的逻辑处理器P中的其他goroutine将迁移到其他OS线程,以便它们可以继续运行。
案例
假设goroutine1在执行文件文件读取操作,则goroutine1会导致内核线程1阻塞,这时候go运行时调度器会把goroutine1所在的逻辑处理器1迁移到其他的内核线程上(这里是内核线程2上),这时候逻辑处理器1上的goroutine2和goroutine3就不会受goroutine1的影响了。
等goroutine1文件读取操作完成后goroutine1又会被go运行时调度系统重新放入到逻辑处理器1的本地队列。
5.7、其他
-
go运行时内核线程(M)的数量默认是10000个,你可以使用runtime/debug包里面的debug.SetMaxThreads(10000)来设置。
-
默认情况下,Go默认是给每个可用的物理处理器都分配一个逻辑处理器(p),如果你需要修改逻辑处理器§个数,可以使用runtime包的runtime.GOMAXPROCS函数设置。
-
goroutine(G)的数量则是由用户程序自己来确定,理论只要内存够大,可以无限制创建。