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 空闲链表
入口 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 routinesgo 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, " + namech <- msg}
Buffered channels limit the amount of messages it can keep.
ch := make(chan int, 2)ch <- 1ch <- 2ch <- 3// fatal error:// all goroutines are asleep - deadlock!
Goroutines 是轻量级的线程,可以参考并发编程导论一文中的进程、线程与协程的讨论;Go 为我们提供了非常便捷的 Goroutines 语法:
// 普通函数func doStuff(s string) {}func main() {// 使用命名函数创建 Goroutinego doStuff("foobar")// 使用匿名内部函数创建 Goroutinego func (x int) {// function body goes here}(42)}
信道(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)// 执行 Goroutinego 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 := <-pingspongs <- 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)}}