在操做系统中,执行体是个抽象的概念。与之对应的实体有进程、线程以及协程(coroutine)。协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特色是 "轻"!能够轻松建立上百万个协程而不会致使系统资源衰竭。
多数编程语言在语法层面并不直接支持协程,而是经过库的方式支持。可是用库的方式支持的功能每每不是很完整,好比仅仅提供轻量级线程的建立、销毁和切换等能力。若是在这样的协程中调用一个同步 IO 操做,好比网络通讯、本地文件读写,都会阻塞其余的并发执行的协程,从而没法达到轻量级线程自己指望达到的目标。html
Golang 在语言级别支持协程,称之为 goroutine。Golang 标准库提供的全部系统调用操做(包括全部的同步 IO 操做),都会出让 CPU 给其余 goroutine。这让 goroutine 的切换管理不依赖于系统的线程和进程,也不依赖于 CPU 的核心数量,而是交给 Golang 的运行时统一调度。算法
goroutine 是 Golang 中并发设计的核心,更多关于并发的概念,请参考《Golang 入门 : 理解并发与并行》。 本文接下来的部分着重经过 demo 介绍 goroutine 的用法。编程
要在一个协程中运行函数,直接在调用函数时添加关键字 go 就能够了:网络
package main import ( "time" "fmt" ) func say(s string) { for i := 0; i < 3; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("hello world") time.Sleep(1000 * time.Millisecond) fmt.Println("over!") }
执行上面的代码,输出结果为:并发
hello world
hello world
hello world
over!
至于为何要在 main 函数中调用 Sleep,如何用优雅的方式代替 Sleep,请参考《Golang 入门 : 等待 goroutine 完成任务》一文。编程语言
让咱们经过下面的 demo 来理解 goroutine 的生命周期:函数
package main import ( "runtime" "sync" "fmt" ) func main() { // 分配一个逻辑处理器给调度器使用 runtime.GOMAXPROCS(1) // wg用来等待程序完成 // 计数加2,表示要等待两个goroutine var wg sync.WaitGroup wg.Add(2) fmt.Println("Start Goroutines") // 声明一个匿名函数,并建立一个goroutine go func(){ // 在函数退出时调用Done来通知main函数工做已经完成 defer wg.Done() // 显示字母表3次 for count := 0; count< 3; count++{ for char := 'a'; char< 'a'+26; char++{ fmt.Printf("%c ", char) } fmt.Println() } }() // 声明一个匿名函数,并建立一个goroutine go func(){ // 在函数退出时调用Done来通知main函数工做已经完成 defer wg.Done() // 显示字母表3次 for count := 0; count< 3; count++{ for char := 'A'; char< 'A'+26; char++{ fmt.Printf("%c ", char) } fmt.Println() } }() // 等待goroutine结束 fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("Terminating Program") }
在 demo 的起始部分,经过调用 runtime 包中的 GOMAXPROCS 函数,把可使用的逻辑处理器的数量设置为 1。
接下来经过 goroutine 执行的两个匿名函数分别输出三遍小写字母和三遍大写字母。运行上面代码,输出的结果以下:spa
Start Goroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
Terminating Program
第一个 goroutine 完成全部任务的时间过短了,以致于在调度器切换到第二个 goroutine 以前,就完成了全部任务。这也是为何会看到先输出了全部的大写字母,以后才输出小写字母。咱们建立的两个 goroutine 一个接一个地并发运行,独立完成显示字母表的任务。
由于 goroutine 以非阻塞的方式执行,它们会随着程序(主线程)的结束而消亡,因此咱们在 main 函数中使用 WaitGroup 来等待两个 goroutine 完成他们的工做,更多 WaitGroup 相关的信息,请参考《Golang 入门 : 等待 goroutine 完成任务》一文。操作系统
基于调度器的内部算法,一个正运行的 goroutine 在工做结束前,能够被中止并从新调度。调度器这样作的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会中止当前正运行的 goroutine,并给其余可运行的 goroutine 运行的机会。
让咱们经过下图来理解这一场景(下图来自互联网):线程
让咱们经过一个运行时间长些的任务来观察该行为,运行下面的 代码:
package main import ( "runtime" "sync" "fmt" ) func main() { // wg用来等待程序完成 var wg sync.WaitGroup // 分配一个逻辑处理器给调度器使用 runtime.GOMAXPROCS(1) // 计数加2,表示要等待两个goroutine wg.Add(2) // 建立两个goroutine fmt.Println("Create Goroutines") go printPrime("A", &wg) go printPrime("B", &wg) // 等待goroutine结束 fmt.Println("Waiting To Finish") wg.Wait() fmt.Println("Terminating Program") } // printPrime 显示5000之内的素数值 func printPrime(prefix string, wg *sync.WaitGroup){ // 在函数退出时调用Done来通知main函数工做已经完成 defer wg.Done() next: for outer := 2; outer < 5000; outer++ { for inner := 2; inner < outer; inner++ { if outer % inner == 0 { continue next } } fmt.Printf("%s:%d\n", prefix, outer) } fmt.Println("Completed", prefix) }
代码中运行了两个 goroutine,分别打印 1-5000 内的素数,输出的结果比较长,精简以下:
Create Goroutines Waiting To Finish B:2 B:3 ... B:3851 A:2 ** 切换 goroutine A:3 ... A:4297 B:3853 ** 切换 goroutine ... B:4999 Completed B A:4327 ** 切换 goroutine ... A:4999 Completed A Terminating Program
上面的输出说明:goroutine B 先执行,而后切换到 goroutine A,再切换到 goroutine B 运行至任务结束,最后又切换到 goroutine A,运行至任务结束。注意,每次运行这个程序,调度器切换的时间点都会稍有不一样。
前面的两个示例,经过设置 runtime.GOMAXPROCS(1),强制让 goroutine 在一个逻辑处理器上并发执行。用一样的方式,咱们能够设置逻辑处理器的个数等于物理处理器的个数,从而让 goroutine 并行执行(物理处理器的个数得大于 1)。
下面的代码可让逻辑处理器的个数等于物理处理器的个数:
runtime.GOMAXPROCS(runtime.NumCPU())
其中的函数 NumCPU 返回可使用的物理处理器的数量。所以,调用 GOMAXPROCS 函数就为每一个可用的物理处理器建立一个逻辑处理器。注意,从 Golang 1.5 开始,GOMAXPROCS 的默认值已经等于可使用的物理处理器的数量了。
修改上面输出素数的程序:
runtime.GOMAXPROCS(2)
由于咱们只建立了两个 goroutine,因此逻辑处理器的数量设置为 2 就能够了,从新运行该程序,看看是否是 A 和 B 的输出混合在一块儿了:
... B:1741 B:1747 A:241 A:251 B:1753 A:257 A:263 A:269 A:271 A:277 B:1759 A:281 ...
除了这个 demo 程序,在真实场景中这种并行的方式会带来不少数据同步的问题。接下来咱们将介绍如何来解决数据的同步问题。
参考:
《Go语言实战》