协程为什么比线程轻量?轻量在何处?
线程属于操作系统的概念,线程切换涉及到系统调用。
协程属于语音内部运行机制的概念。
协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
内核空间:操作系统空间;用户空间:程序空间。
协程切换比线程切换快主要有两点:
(1)协程切换完全在用户空间进行,线程切换涉及特权模式切换,需要在内核空间完成;
(2)协程切换相比线程切换做的事情更少。
协程切换只涉及基本的 CPU 上下文切换,所谓的 CPU 上下文,就是一堆寄存器,里面保存了 CPU 运行任务所需要的信息:从哪里开始运行(%rip:指令指针寄存器,标识 CPU 运行的下一条指令),栈顶的位置(%rsp: 是堆栈指针寄存器,通常会指向栈顶位置),当前栈帧在哪(%rbp 是栈帧指针,用于标识当前栈帧的起始位置)以及其它的 CPU 的中间状态或者结果(%rbx,%r12,%r13,%14,%15 等等)。协程切换非常简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。而且完全在用户态进行,一般来说一次协程上下文切换最多就是几十 ns 这个量级。下面给出 libco 的协程切换的汇编代码,也就是二十来条汇编指令,完成当前协程 CPU 寄存器的保存,并恢复调度进来的 CPU 寄存器状态,类似的也可以参考 boost context 里面的切换汇编代码,大同小异。
线程切换系统内核调度的对象是线程,因为线程是调度的基本单元(进程是资源拥有的基本单元,进程的切换需要做的事情更多,这里占时不讨论进程切换),而线程的调度只有拥有最高权限的内核空间才可以完成,所以线程的切换涉及到用户空间和内核空间的切换,也就是特权模式切换,然后需要操作系统调度模块完成线程调度(taskstruct),而且除了和协程相同基本的 CPU 上下文,还有线程私有的栈和寄存器等,说白了就是上下文比协程多一些,其实简单比较下 task_strcut 和 任何一个协程库的 coroutine 的 struct 结构体大小就能明显区分出来。而且特权模式切换
讲讲 Go 的 select 底层数据结构和一些特性?(难点,没有项目经常可能说不清,面试一般会问你项目中怎么使用 select)
答:go 的 select 为 golang 提供了多路 IO 复用机制,和其他 IO 复用一样,用于检测是否有读写事件是否 ready。linux 的系统 IO 模型有 select,poll,epoll,go 的 select 和 linux 系统 select 非常相似。
select 结构组成主要是由 case 语句和执行的函数组成 select 实现的多路复用是:每个线程或者进程都先到注册和接受的 channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。
gmp 模型
2 个协程交替打印奇数偶数
package main
import (
"fmt"
"sync"
)
//打印奇数
func PrintOddNumber(wg *sync.WaitGroup, ch chan int, num int) {
defer wg.Done()
for i := 0; i <= num; i++ {
ch <- i
if i%2 != 0 {
fmt.Println("奇数:", i)
}
}
}
//打印偶数
func PrintEvenNumber(wg *sync.WaitGroup, ch chan int, num int) {
defer wg.Done()
for i := 1; i <= num; i++ {
<-ch
if i%2 == 0 {
fmt.Println("偶数:", i)
}
}
}
func main() {
wg := sync.WaitGroup{}
ch := make(chan int)
wg.Add(1)
go PrintOddNumber(&wg, ch, 10)
go PrintEvenNumber(&wg, ch, 10)
wg.Wait()
}
Golang 的 GMP 调度模型(Golang 面试必问)
Golang 的 GMP 模型与 Java 的线程池+runnable 有何区别?
极其相似,Golang 的协程就是 Java 的 runnable。
区别如下:
- Golang 的协程队列分为局部队列和全局队列。Java 的 Runnable 一般只有一个全局队列。Golang 的线程池增加线程的机制是当线程遇到 IO 阻塞挂起的时候才会增加新的线程。
- 并发执行并不是单纯的并发,还涉及到并发任务之间的同步、通信等操作,golang 的 mutex、channel 等机制都是对应协程的,这些机制在 Java 中并没有对等事物。
Golang 命令行工具探测 GMP 模型
介绍一下 golang 调度器相关的 DEBUG 工具# 启动程序
GOMAXPROCS=8 GODEBUG=schedtrace=500 godoc -http=:8888
# 部分结果
SCHED 4554ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=40 [4 1 17 17 7 1 3 18]
SCHED 5056ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=3 [1 4 2 1 0 0 2 1]
SCHED 5558ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=12 [0 2 0 6 2 0 0 0]
SCHED 6064ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=13 [3 0 12 0 11 0 0 0]
SCHED 6569ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=22 [0 3 17 11 2 3 2 3]
SCHED 7073ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=3 [20 1 3 7 1 2 2 2]
SCHED 7582ms: gomaxprocs=8 idleprocs=0 threads=22 spinningthreads=0 idlethreads=11 runqueue=5 [0 1 0 4 0 0 1 8]
压力工具命令为GOMAXPROCS=1 httpit http://localhost:8888/pkg/ -c300
一共会使用 300 个连接。程序使用了 GOMAXPROCS=8,也就是 8 个 P, GODEBUG=schedtrace=500
则表示每 500ms 打印一次调度结果。结果的字段解释如下:idleprocs 空闲的 P 数量 threads 开启的线程 M 数量 spinningthreads 自旋的 M 数量 idlethreads 空闲(休眠)的 M 数量 runqueue 全局队列任务数量[4 1 17 17 7 1 3 18]
每个 P 当前的任务数量从结果中可以看出虽然用户数(连接数)是 300,但是程序创建的 M(线程)数量只有 22 个,线程复用率还是蛮高的。全局队列和 P 的本地队列会趋于均匀化