相信很多读者和我一样,也都是冲着 Go 大肆宣扬的高并发而入坑,从源码的角度来看,协程goroutine本质是由谷歌实现的超级“线程池”。每个goroutine大约 4-5KB的栈内存消耗的确更加符合目前web服务的场景,和由于实现机制和轻量级的开销,Go的高并发性的确有目共睹。

goroutine

在我们使用JAVA或者C编写网络程序时,基本都是用一个线程来处理一个http请求, 但是这样的方式对于资源的利用率不高。而Go语言就实现了这样一种轻量级线程的机制,GO语言在底层封装了所有的系统调用,自己实现了一个调度器,这种设计在操作系统的代码中非常多见,比如现代的操作系统基本都会封装一个软件的Timer,同时可以提供上万个软Timer同时工作,而这只是基于数量很少的硬件timer实现的,而GO语言中的并发也是如此,他是基于线程的调度池,这种调度的单元在Go语言中被称为goroutine。

一、goroutine的启动

Go 程序中使用 go 关键字为一个函数创建一个 goroutine。goroutine 必定对应一个函数。为一个普通函数创建 goroutine 的方式如下:

go 函数名( 参数列表 )

当然也可以使用匿名函数来创建goroutine方式如下:

go func( 参数列表 ){
    函数体
}( 调用参数列表 )
注意使用匿名来启动goroutine是可以和闭包结合的

下面我们来举例说明,在下面的例程当中我们先通过running函数来创建一个goroutine1,并使用匿名函数来创造了另一个goroutine2,并且在最后使用sleep功能阻塞主函数,给两个子goroutine以运行的机会。

package main

import (
	"fmt"
	"time"
)

func running() {
	var times int
	for {// 构建一个无限循环
		times++
		fmt.Println("this is runing function's tick", times)//从笔者的机器上看runing函数中的ticks累加到300左右
	}
}
func main() {

	go running() //通过普通函数调用goroutine
	go func() {  //通过匿名函数调用goroutine
		var times int //注意这个times变量其实和匿名函数共同构成闭包
		for {
			times++
			fmt.Println("this is anonymous function's tick ", times)//从笔者的机器上看runing函数中的ticks累加到300左右
		}
	}()
	time.Sleep(time.Millisecond)//在本例中使用休眠的方式来阻塞main函数创建的主goroutine

}

二、深入理解goroutine的调度机制

1.例程的执行时序

刚刚的例程中程序运行的时序图如下,这个例子中,G程序在启动时会创建一个main() 函数的goroutine也称主goroutine。在 main() 函数执行到 go running 语句时,归属于 running() 函数的 goroutine 被创建,执行到go func 语句时,归属于匿名函数的 goroutine 被创建,这两个goroutine实际都是由主goroutine创建的,在主goroutine未放弃CPU的情况下,子goroutine不会被调度执行,直到执行sleep(time.Millisecond)主goroutine被阻塞,处于就绪态的子goroutine才有执行机会,而当sleep到时后,主 goroutine退出运行态,此时两个子Goroutine也随之退出。具体如下图:

 

Go语言高并发与微服务实战电子书_Go

2.goroutine的调度策略

这里再为各位读者补充一下必要的操作系统知识,与一般操作系统一样,协程也将运行状态抽象归为三种:

就绪(Ready):该goroutine在就绪列表中,只等待CPU。

运行(Running):该goroutine正在执行。

阻塞(Blocked):该goroutine不在就绪列表中。包含挂起、延时、等待信号量、等待事件等等。

Go语言高并发与微服务实战电子书_高并发_02

下次我们再来探讨一下GO语言中是不是只有通过time.sleep才能让子的goroutine得以执行。