1、进程、线程是什么
多个线程可以属于同一个进程并共享内存空间。因为多线程不需要创建新的虚拟内存空间,所以它们也不需要内存管理单元处理上下文的切换,线程之间的通信也正是基于共享的内存进行的,与重量级的进程相比,线程显得比较轻量。
虽然线程比较轻量,但是在调度时也有比较大的额外开销。每个线程会都占用 1M 以上的内存空间,在切换线程时不止会消耗较多的内存,恢复寄存器中的内容还需要向操作系统申请或者销毁资源,每一次线程上下文的切换都需要消耗 ~1us 左右的时间1,但是 Go 调度器对 Goroutine 的上下文切换约为 ~0.2us,减少了 80% 的额外开销
2、协程是怎么实现的
goroutine是由Go运行时管理的用户层轻量级线程。相较于操作系统线程,goroutine的资源占用和使用代价都要小得多。我们可以创建几十个、几百个甚至成千上万个goroutine。
将这些goroutine按照一定算法放到CPU上执行的程序就称为goroutine调度器(goroutine scheduler)。一个Go程序对于操作系统来说只是一个用户层程序,操作系统眼中只有线程,goroutine的调度全要靠Go自己完成。
G-P-M模型
关于G、P、M的定义,可以参见$GOROOT/src/runtime/runtime2.go这个源文件。
-
G — 表示 Goroutine,它是一个待执行的任务;
-
M — 表示操作系统的线程,它由操作系统的调度器调度和管理;
Go 语言并发模型中的 M 是操作系统线程。调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用),最多只会有GOMAXPROCS
个活跃线程能够正常运行。 -
P — 表示处理器,它可以被看做运行在线程上的本地调度器;
如何执行Goroutine:- 有一定几率从全局队列获取Goroutine
- 从本地运行队列获取Goroutine
- 从其他处理器窃取代运行的Goroutine
抢占式调度
最开始的时候:
不支持抢占式调度,这导致一旦某个G中出现死循环的代码逻辑,那么G将永久占用分配给它的P和M。
Go 1.2
抢占式调度的原理是:在每个函数或方法的入口加上一段额外的代码,让运行时有机会检查是否需要执行抢占调度。而是否需要被强占是有个监控线程做检查并设置标记的。
在Go程序启动时,运行时会启动一个名为sysmon的M(一般称为监控线程)
sysmon每20us~10ms启动一次。
// $GOROOT/src/runtime/proc.go
// forcePreemptNS是在一个G被抢占之前给它的时间片
const forcePreemptNS = 10 * 1000 * 1000 // 10ms
func retake(now int64) uint32 {
...
// 抢占运行时间过长的G
t := int64(_p_.schedtick)
if int64(pd.schedtick) != t {
pd.schedtick = uint32(t)
pd.schedwhen = now
continue
}
if pd.schedwhen+forcePreemptNS > now {
continue
}
preemptone(_p_)
...
}
- 编译器会在调用函数前插入
runtime.morestack
; - Go 语言运行时会在垃圾回收暂停程序、系统监控发现 Goroutine 运行超过 10ms 时发出抢占请求
StackPreempt
; - 当发生函数调用时,可能会执行编译器插入的
runtime.morestack
,它调用的runtime.newstack
会检查 Goroutine 的stackguard0
字段是否为StackPreempt
; - 如果
stackguard0
是StackPreempt
,就会触发抢占让出当前线程;
Go 1.14
https://cloud.tencent.com/developer/article/1836265
channel阻塞或网络I/O情况下的调度
如果G被阻塞在某个channel操作或网络I/O操作上,那么G会被放置到某个等待队列中,而M会尝试运行P的下一个可运行的G。如果此时P没有可运行的G供M运行,那么M将解绑P,并进入挂起状态。
系统调用阻塞情况下的调度
如果G被阻塞在某个系统调用上,那么不仅G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一起进入阻塞状态。如果此时有空闲的M,则P会与其绑定并继续执行其他G;如果没有空闲的M,但仍然有其他G要执行,那么就会创建一个新M(线程)。当系统调用返回后,阻塞在该系统调用上的G会尝试获取一个可用的P,如果有可用P,之前运行该G的M将绑定P继续运行G;如果没有可用的P,那么G与M之间的关联将解除,同时G会被标记为runnable,放入全局的运行队列中,等待调度器的再次调度。