Go语言创建协程(goroutine)的2种方法(附带实例)
在 Go语言中,协程(goroutine)不仅是轻量级线程,而且是用户级线程。
用户级线程是指由用户控制代码的执行流程,不需要操作系统进行调度和分派。也就是说,Go 程序智能地将协程中的任务合理分配给每个 CPU。
Go 语言不仅有协程,还有用于调度协程的、对接系统级线程的调度器。调度器主要负责统筹调配 Go 语言并发编程模型(即 GPM 模型)中的 3 个主要元素,它们分别是 G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩写)。
其中,M 指的是系统级线程;P 指的是能够使若干个 G 在恰当的时机与 M 对接,并得以运行的中介。
Go 程序在启动时,从 main 包的 main() 函数开始,为 main() 函数创建一个默认的协程。下面讲解两种创建协程方法:
使用 go 关键字为普通函数创建协程的语法格式如下:
当使用 go 关键字创建协程时,将忽略被调用函数的返回值。
【实例 1】实现并发编程。现有一辆只为上下车的乘客提供一个车门的公交车,这辆公交车行至某一站台后,有 5 位乘客下车,有 5 位乘客上车。无论是下车还是上车,乘客与乘客之间的时间间隔都是 1s。编写 Go 程序通过并发编程实现上述的上下车问题,代码如下:
当使用 go 关键字创建协程时,因为忽略被调用函数的返回值,所以只考虑是否需要为被调用函数设置参数。
下例演示当创建协程时,如何为被调用函数设置参数。
【实例 2】确认上下车的乘客姓名。现有一辆只为上下车的乘客提供一个车门的公交车,这辆公交车行至某一站台后,有 5 位乘客下车,这 5 位乘客的姓名依次为“David”、“Leon”、“Steven”、“James”和“Tom”;有 5 位乘客上车,这 5 位乘客的姓名依次为“张三”、“李四”、“王五”、“赵六”和“周七”。不论是下车还是上车,乘客与乘客之间的时间间隔都是 1s。
编写 Go 程序,通过并发编程实现上述的上下车问题,并确认上下车的乘客姓名。代码如下:
使用 go 关键字为匿名函数创建协程的语法格式如下:
【实例 3】演示为匿名函数创建协程。已知在使用左手画圆的同时,使用右手画方,并且圆和方的个数均为 3 个。定义一个用于实现“左手画圆”的匿名函数。编写 Go 程序,在为匿名函数创建协程后,并发执行程序,实现“左手画圆,右手画方”的效果,并记录圆和方的绘制过程。代码如下:
如果要求把圆绘制成红色,那么就需要使用有参数的匿名函数。这时,除了需要在 func 关键字后面的小括号内添加参数,还需要在匿名函数末端的小括号内为其设置被调用时所需的参数。修改上面的程序,“把圆绘制成红色”的代码如下:
用户级线程是指由用户控制代码的执行流程,不需要操作系统进行调度和分派。也就是说,Go 程序智能地将协程中的任务合理分配给每个 CPU。
Go 语言不仅有协程,还有用于调度协程的、对接系统级线程的调度器。调度器主要负责统筹调配 Go 语言并发编程模型(即 GPM 模型)中的 3 个主要元素,它们分别是 G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩写)。
其中,M 指的是系统级线程;P 指的是能够使若干个 G 在恰当的时机与 M 对接,并得以运行的中介。
Go 程序在启动时,从 main 包的 main() 函数开始,为 main() 函数创建一个默认的协程。下面讲解两种创建协程方法:
- 一种是为普通函数创建协程;
- 另一种是为匿名函数创建协程。
为普通函数创建协程
在 Go 程序中,使用 go 关键字为普通函数创建协程。需要特别注意的是,可以为一个普通函数创建多个协程;但一个协程必定对应一个普通函数。使用 go 关键字为普通函数创建协程的语法格式如下:
go 函数名称(parameter)parameter 是参数列表。
当使用 go 关键字创建协程时,将忽略被调用函数的返回值。
【实例 1】实现并发编程。现有一辆只为上下车的乘客提供一个车门的公交车,这辆公交车行至某一站台后,有 5 位乘客下车,有 5 位乘客上车。无论是下车还是上车,乘客与乘客之间的时间间隔都是 1s。编写 Go 程序通过并发编程实现上述的上下车问题,代码如下:
package main //声明 main 包 import ( "fmt" //导入 fmt 包,用于打印字符串 "time" //导入 time 包,用于延时 ) func getOff(){ //循环 5 次 for i:= 5; i> 0; i--{ fmt.Println("还有",i,"位乘客下车") //延时 1s time.Sleep(1 * time.Second) } } func main(){ //执行并发程序 go getOff() //循环 5 次 for i:= 1; i< 6; i++{ fmt.Println("第",i,"位乘客上车") //延时 1s time.Sleep(1 * time.Second) } }运行结果如下(不唯一):
第 1 位乘客上车
还有 5 位乘客下车
还有 4 位乘客下车
第 2 位乘客上车
第 3 位乘客上车
还有 3 位乘客下车
还有 2 位乘客下车
第 4 位乘客上车
第 5 位乘客上车
还有 1 位乘客下车
当使用 go 关键字创建协程时,因为忽略被调用函数的返回值,所以只考虑是否需要为被调用函数设置参数。
下例演示当创建协程时,如何为被调用函数设置参数。
【实例 2】确认上下车的乘客姓名。现有一辆只为上下车的乘客提供一个车门的公交车,这辆公交车行至某一站台后,有 5 位乘客下车,这 5 位乘客的姓名依次为“David”、“Leon”、“Steven”、“James”和“Tom”;有 5 位乘客上车,这 5 位乘客的姓名依次为“张三”、“李四”、“王五”、“赵六”和“周七”。不论是下车还是上车,乘客与乘客之间的时间间隔都是 1s。
编写 Go 程序,通过并发编程实现上述的上下车问题,并确认上下车的乘客姓名。代码如下:
package main //声明 main 包 import ( "fmt" //导入 fmt 包,用于打印字符串 "time" //导入 time 包,用于延时 ) func getOff(names []string){ //遍历存储下车乘客的姓名的切片中的元素 for i, name := range names { fmt.Println("第",i+1,"位乘客",name,"正在下车") //延时 1s time.Sleep(1 * time.Second) } } func main(){ //存储下车乘客的姓名的切片 var offNames = [5]string{"David","Leon","Steven","James","Tom"} //执行并发程序 go getOff(offNames[:]) //存储上车乘客的姓名的切片 var onNames = []string{"张三","李四","王五","赵六","周七"} //遍历存储上车乘客的姓名的切片中的元素 for i, name := range onNames { fmt.Println("第",i+1,"位乘客",name,"正在上车") //延时 1s time.Sleep(1 * time.Second) } }运行结果如下(不唯一):
第 1 位乘客 张三 正在上车
第 1 位乘客 David 正在下车
第 2 位乘客 Leon 正在下车
第 2 位乘客 李四 正在上车
第 3 位乘客 王五 正在上车
第 3 位乘客 Steven 正在下车
第 4 位乘客 赵六 正在上车
第 4 位乘客 James 正在下车
第 5 位乘客 Tom 正在下车
第 5 位乘客 周七 正在上车
为匿名函数创建协程
在 Go 程序中,使用 go 关键字还可以为匿名函数创建协程。注意,go 关键字的后面须包含两个内容:一个是定义的匿名函数;另一个是匿名函数的调用参数。使用 go 关键字为匿名函数创建协程的语法格式如下:
go func(parameter){ func field }(para)参数说明如下:
- func:Go 语言的关键字,用于定义匿名函数;
- parameter:参数列表;
- func field:匿名函数的实现代码;
- para:匿名函数被调用时所需设置的参数。
【实例 3】演示为匿名函数创建协程。已知在使用左手画圆的同时,使用右手画方,并且圆和方的个数均为 3 个。定义一个用于实现“左手画圆”的匿名函数。编写 Go 程序,在为匿名函数创建协程后,并发执行程序,实现“左手画圆,右手画方”的效果,并记录圆和方的绘制过程。代码如下:
package main import ( "fmt" "time" ) func main(){ //为匿名函数创建协程 go func(){ //循环 3 次 for i:= 0; i< 3; i++{ fmt.Println("左手画圆") //延时 1s time.Sleep(1 * time.Second) } }() //匿名函数被调用时所需设置的参数 //循环 3 次 for i:= 0; i< 3; i++{ fmt.Println("右手画方") //延时 1s time.Sleep(1 * time.Second) } }运行结果如下(不唯一):
右手画方
左手画圆
左手画圆
左手画圆
右手画方
右手画方
左手画圆
如果要求把圆绘制成红色,那么就需要使用有参数的匿名函数。这时,除了需要在 func 关键字后面的小括号内添加参数,还需要在匿名函数末端的小括号内为其设置被调用时所需的参数。修改上面的程序,“把圆绘制成红色”的代码如下:
package main import ( "fmt" "time" ) func main(){ //为匿名函数创建协程 go func(color string){ //循环 5 次 for i:= 0; i < 3; i++ { fmt.Println("左手画圆,圆的颜色是" + color) //延时 1s time.Sleep(1 * time.Second) } }("红色")//匿名函数被调用时所需设置的参数 //循环 5 次 for i:= 0; i < 3; i++ { fmt.Println("右手画方") //延时 1s time.Sleep(1 * time.Second) } }运行结果如下(不唯一):
右手画方
左手画圆,圆的颜色是红色
左手画圆,圆的颜色是红色
左手画圆,圆的颜色是红色
右手画方
右手画方
左手画圆,圆的颜色是红色