Go-ConcurrentProgramming-CheatSheet

Go Concurrent Programming CheatSheet | Go 并发编程实践与机制探究

协程调度

Golang 简称 Go,Go 的协程(goroutine) 和我们常见的线程(Thread)一样,拥有其调度器。

G (Goroutine),代表协程,也就是每次代码中使用 go 关键词时候会创建的一个对象 M (Work Thread),工作线程 P (Processor),代表一个处理器,又称上下文

每一个运行的 M 都必须绑定一个 P,线程 M 创建后会去检查并执行 G (goroutine)对象 每一个 P 保存着一个协程 G 的队列 除了每个 P 自身保存的 G 的队列外,调度器还拥有一个全局的 G 队列 M 从队列中提取 G,并执行 P 的个数就是 GOMAXPROCS(最大 256),启动时固定的,一般不修改,go 1.5 版本之前的 GOMAXPROCS 默认是 1,go 1.5 版本之后的 GOMAXPROCS 默认是 CPU 的数目。 M 的个数和 P 的个数不一定一样多(会有休眠的 M 或 P 不绑定 M)(最大 10000) P 是用一个全局数组(255)来保存的,并且维护着一个全局的 P 空闲链表

image

Goroutine 的入队与执行

入口 main 函数,其实是作为一个 goroutine 来执行,程序启动的时候,首先跑的是主线程,然后这个主线程会绑定第一个 P。

当我们创建一个 G 对象,就是 gorutine,它会加入到本地队列或者全局队列。如果还有空闲的 P,则创建一个 M 绑定该 P;注意,无论在哪个 M 中创建了一个 G,只要 P 有空闲的,就会引起新 M 的创建。新创建的 M 所绑的 P 的初始化队列会从其他 G 队列中取任务过来。

M 会启动一个底层线程,循环执行能找到的 G 任务,其依次从当前 M 所绑的 P 队列中找,去别的 P 的队列中找,去全局 G 队列中找。

协程的切换时间片是 10ms,也就是说 goroutine 最多执行 10ms 就会被 M 切换到下一个 G。这个过程,又被称为 中断,挂起。协程序启动时会首先创建一个特殊的内核线程 sysmon,用来监控和管理,其内部是一个循环:记录所有 P 的 G 任务的计数 schedtick,schedtick 会在每执行一个 G 任务后递增。如果检查到 schedtick 一直没有递增,说明这个 P 一直在执行同一个 G 任务,如果超过 10ms,就在这个 G 任务的栈信息里面加一个 tag 标记。然后这个 G 任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个 G。如果没有遇到非内联函数 调用的话,那就会一直执行这个 G 任务,直到它自己结束;如果是个死循环,并且 GOMAXPROCS=1 的话。那么一直只会只有一个 P 与一个 M,且队列中的其他 G 不会被执行!

func main(){
runtime.GOMAXPROCS(1)
go func(){
// 永远不会输出
fmt.Println("hello world")
}()
go func(){
for {
}
}()
select {}
}

中断的时候将寄存器里的栈信息,保存到自己的 G 对象里面当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行。

同步

Channels are concurrency-safe communication objects, used in goroutines.

func main() {
// A "channel"
ch := make(chan string)
// Start concurrent routines
go push("Moe", ch)
go push("Larry", ch)
go push("Curly", ch)
// Read 3 results
// (Since our goroutines are concurrent,
// the order isn't guaranteed!)
fmt.Println(<-ch, <-ch, <-ch)
}
func push(name string, ch chan string) {
msg := "Hey, " + name
ch <- msg
}

Buffered channels limit the amount of messages it can keep.

ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3
// fatal error:
// all goroutines are asleep - deadlock!

并发编程

Goroutines

Goroutines 是轻量级的线程,可以参考并发编程导论一文中的进程、线程与协程的讨论;Go 为我们提供了非常便捷的 Goroutines 语法:

// 普通函数
func doStuff(s string) {
}
func main() {
// 使用命名函数创建 Goroutine
go doStuff("foobar")
// 使用匿名内部函数创建 Goroutine
go func (x int) {
// function body goes here
}(42)
}

Channels

信道(Channel)是带有类型的管道,可以用于在不同的 Goroutine 之间传递消息,其基础操作如下:

// 创建类型为 int 的信道
ch := make(chan int)
// 向信道中发送值
ch <- 42
// 从信道中获取值
v := <-ch
// 读取,并且判断其是否关闭
v, ok := <-ch
// 读取信道,直至其关闭
for i := range ch {
fmt.Println(i)
}

譬如我们可以在主线程中等待来自 Goroutine 的消息,并且输出:

// 创建信道
messages := make(chan string)
// 执行 Goroutine
go func() { messages <- "ping" }()
// 阻塞,并且等待消息
msg := <-messages
// 使用信道进行并发地计算,并且阻塞等待结果
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从 c 中接收

如上创建的是无缓冲型信道(Non-buffered Channels),其是阻塞型信道;当没有值时读取方会持续阻塞,而写入方则是在无读取时阻塞。我们可以创建缓冲型信道(Buffered Channel),其读取方在信道被写满前都不会被阻塞:

ch := make(chan int, 100)
// 发送方也可以主动关闭信道
close(ch)

Channel 同样可以作为函数参数,并且我们可以显式声明其是用于发送信息还是接收信息,从而增加程序的类型安全度:

// ping 函数用于发送信息
func ping(pings chan<- string, msg string) {
pings <- msg
}
// pong 函数用于从某个信道中接收信息,然后发送到另一个信道中
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
func main() {
pings := make(chan string, 1)
pongs := make(chan string, 1)
ping(pings, "passed message")
pong(pings, pongs)
fmt.Println(<-pongs)
}

同步

同步,是并发编程中的常见需求,这里我们可以使用 Channel 的阻塞特性来实现 Goroutine 之间的同步:

func worker(done chan bool) {
time.Sleep(time.Second)
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
// 阻塞直到接收到消息
<-done
}

Go 还为我们提供了 select 关键字,用于等待多个信道的执行结果:

// 创建两个信道
c1 := make(chan string)
c2 := make(chan string)
// 每个信道会以不同时延输出不同值
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
// 使用 select 来同时等待两个信道的执行结果
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}