介绍
先介绍一下常见的三种线程模型,然后再介绍Go中独特的线程模型
三种线程模型
线程的并发执行是由操作系统来调度的,然而操作系统一般在内核提供对线程的支持,我们在编程的过程中创建的线程是用户线程,用户线程与内核线程有什么关系呢?下面要讲解的三种线程模型是根据用户线程和内核线程关系的不用而区分的
一对一模型
这种线程模型是用户线程与内核线程是一一对应的,当前进程对应的主线程就是用户线程,也就是对应一个内核线程,如下图所示:
一对一模型.png
这种线程模型的优点在于在多处理器上,多个线程可以真正地实现并行,而不是只有并发。并且当一个线程由于执行系统调用等原因被阻塞的时候,其他的线程不受影响。
缺点在于一般操作系统会限制内核线程的个数,所以用户线程的个数也会受到限制。另外由于用户线程与系统线程一一对应的关系,当用户线程执行系统调用的时候,需要从用户态执行的程序切换到内核态,等执行完系统调用后,又会从内核态切换回用户态,而这个切换的代价是非常大的。
多对一模型
多对一模型是指多个用户线程对应一个内核线程,同一时刻同一个内核线程只能对应一个用户线程。这时候对应同一个内核线程的多个用户线程的上下文切换是在用户态进行的,不由操作系统调用的。其模型图如下:
多对一模型
这种模型的好处在于上下文切换是在用户态进行的,所以切换的速度很快,开销很小。可创建的用户线程也可以很多,只受内存的大小限制。这种模型由于是多个用户线程对应一个内核线程,当该内核线程对应的一个用户线程被阻塞挂起的时候,该内核线程的其他用户线程也不能运行了。另外这种模型并不能很好地利用多核CPU进行并行运行。
多对多模型
多对多模型结合上述两种模型的特点,让大量的用户线程对应少数几个内核线程,其模型图如下:
多对多模型
这时候每个内核线程对应多个用户线程,每个用户线程可以对应多个内核线程。当一个用户线程阻塞后,其对应的内核线程会被阻塞,但是被阻塞的内核线程的其他用户线程可以切换到其他内核线程上继续运行,所以多对多模型是可以充分利用多核CPU提升运行效率的。多对多模型对用户线程的个数没有限制,理论上只要内存足够就可以。
Go线程模型
Go线程模型属于多对多线程模型,其模型图如下:
Go线程模型
Go语言中使用go语句创建的goroutine可以认为是轻量级的用户线程(协程也是轻量级的用户线程),go线程模型包含三个概念:内核线程(M),goroutine(G),逻辑处理器(P),在Go中每个逻辑处理器(P)会绑定到某一个内核线程(M)上,每个逻辑处理器(P)内有一个本地队列,用来存放goroutine。在上面介绍的多对多线程模型中是操作系统调度线程在物理CPU上运行,在Go中则是Go在运行时调度goroutine在逻辑处器(P)上运行的。
在Go中存在两极调度,一级是操作系统的调度,该调度系统调度逻辑处理器占用CPU时间片运行的,还有一级是Go的运行时的调度系统,该调度系统调度某个goroutine在逻辑处理器(P)上运行。
使用Go语句创建一个goroutine后,创建的goroutine会被放入Go运行时调度器的全局队列中,然后Go运行时调度器会把全局队列中的goroutine分配给不同的逻辑处理器(P),分配的goroutine会被放到逻辑处理器(P)的本地队列中,当本地队列中里的goroutine就绪并且待分配后,就会被分配到逻辑处理器(P)上运行。如上图goroutine1当前正在占用逻辑处理器1在运行。
这里需要注意的是,为了避免某些goroutine出现饥饿现象,被分配到某一个逻辑处理器(P)上的多个goroutine是分时在该逻辑处理器(P)上运行的,而不是独占逻辑处理器(P)运行到结束。比如某个goroutine从开始运行到结束运行需要5分钟,那么当前逻辑处理器(P)下的另外其他goroutine并不是顺序执行的,而是交叉并发执行的。
goroutine内部实现与在多个操作系统线程之间复用的协程(coroutines)一样,如果有一个goroutine阻塞操作系统线程,则该操作系统线程对应的逻辑处理器(P)中其他的goroutine将迁移到其他操作系统线程中,以便它们可以继续运行,如下图所示:
切换逻辑处理器
如上图左图,假设goroutine1在执行某些系统调用的时候,则goroutine1会导致内核线程1阻塞,这时候Go运行时调度器会把goroutine1所在的逻辑处理器1迁移到其他的内核线程上,这时候逻辑处理1上的goroutine2和goroutine3就不会受goroutine1的影响了。等goroutine1的系统调用执行完后,goroutine1又会被Go运行时调度系统重新放入到逻辑处理器1的本地队列。
默认情况下,Go是给每个可用的物理处理器都分配一个逻辑处理器(P),如果需要修改逻辑处理器(P)的个数可以通过函数runtime.GOMAXPROCS函数。
goroutine(G)的数量也是可以创建很多个,理论只要内存够大,可以无限制创建。
总结
正是由于这样的线程模型才让Go中每台机器可以创建成千上万的goroutine。我们也需要特别了解其中的PMG模型。
2021.9.30 16:29 深圳
网友评论