协程是Go语言的关键特性,主要用于并发编程,协程是一种轻量级的线程,因为协程开销比较小,所以创建上万的协程也不是什么难事,下面介绍协程的基本用法。
1.创建并运行协程
创建协程非常简单,只要一个关键词go和一个函数,就可以创建并运行协程。
语法:
go f()
通过go关键词创建一个协程,并在新创建的协程中运行函数 f()
例子:
package main
import (
"fmt"
"time"
)
// 定义一个函数,循环打印5次字符串参数s
func say(s string) {
for i := 0; i < 5; i++ {
// 当前协程休眠100毫秒
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
// 程序启动的时候,首先创建一个主协程,运行main函数
func main() {
// 创建一个协程,运行say函数,传入参数"world"
go say("world")
// 在主协程中运行say函数,传入参数hello
say("hello")
}
运行输出如下:
world
hello
world
hello
world
hello
world
hello
world
hello
因为协程并发执行缘故,hello和world交叉输出,而且有一定的概率出现say("hello")函数先执行完成,程序就退出了,导致say("world")函数没有执行完成。
说明:因为say("hello")函数是在主协程中运行的,如果say("hello")函数先执行完成,那么主协程就会退出,程序就结束了,其他未执行完成的协程也会强制退出,后面介绍如何通过channel解决这种情况。
2.协程通信
协程之间通信主要有两种方式:
- 共享全局变量
- channel
因为协程是在同一个进程空间中运行,所以可以共享变量,但是使用共享变量方式通信,因为并发问题,为了保证数据原子性,需要加锁处理。
通过共享变量通信的例子:
package main
import (
"fmt"
"sync"
"time"
)
// 计数器
var count int
// 创建互斥对象,通过互斥对象加锁保护count变量,同一时间只能一个协程访问
var mu sync.Mutex
func main() {
for i := 0; i < 2 ; i++ {
// 循环创建两个协程,执行匿名函数
go func() {
// 加锁
mu.Lock()
// 延迟释放锁 - 匿名函数执行结束才会释放锁
defer mu.Unlock()
// 下面的代码同一时间只有一个协程在运行
// 对count累加计数
for j :=0 ; j < 100 ; j++ {
count++
// 休眠10毫秒
time.Sleep(10 * time.Millisecond)
}
}()
}
// 先休眠5秒,等前面的协程执行结束
time.Sleep(5 * time.Second)
// 打印计数值
fmt.Println(count)
}
运行输出:200
如果你没加锁,直接对count进行累加,输出的结果就不一定是200了,有兴趣可以注释掉,加锁的代码,调试一下。
3.channel
channel,可以翻译成通道,是go语言推荐的协程之间的通信机制,channel的通信方式可以形象的想象成一根空心的管道,从一头塞数据进去,从另外一头读取数据,协程通过channel通信可以避开加锁操作。
3.1. 创建channel
channel是拥有数据类型的,channel只能传递指定的数据类型的值。
通过make创建channel
语法:
c := make(chan 数据类型)
例子:
// 创建int类型的channel
c := make(chan int)
3.2.读取channel中的数据
// 从channel变量c中读取数据,保存到变量v中
v := <-c
// 从channel变量c中读取数据,数据直接丢弃
<-c
提示:如果channel中没有数据,会阻塞协程,直到channel有数据或者channel关闭。
3.3.往channel中写数据
// 往channel变量c中,写入int数据100
c <- 100
channel有两种类型:
- 无缓冲channel
- 缓冲channel
无缓冲的意思就是channel只能保存一个数据,当往无缓冲的channel写入第2个数据的时候协程会被阻塞,直到channel中的第1个数据被取走,才会唤醒被阻塞的协程。
缓冲channel,指的是channel中有一个缓冲队列,当写入的数据没有塞满这个缓冲队列之前,往channel写数据协程是不会被阻塞的,如果取数据的速度比写数据的速度快,那么永远不会阻塞写操作。
是否缓冲channel,写操作上区别就是什么时候会阻塞写操作。
前面通过 make(chan 数据类型) 语法 创建的channel就是无缓冲的channel,后面会介绍缓冲channel
3.4.channel例子
package main
import "fmt"
// 定义累加函数,负责将s数组所有值相加, sum函数还接受一个int类型的channel参数c
func sum(s []int, c chan int) {
sum := 0
// 累加数组s的所有值
for _, v := range s {
sum += v
}
// 将计算的结果发生到channel中
c <- sum
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
// 定义一个int类型channel,用来接收协程计算结果
c := make(chan int)
// 创建第1协程,计算数组前半部分的累加值
go sum(s[:len(s)/2], c)
// 创建第2个协程,计算数组后半部分的累加值
go sum(s[len(s)/2:], c)
// 通过channel接收,两个协程的并发计算结果
// 这里读取两次channel
x := <-c
y := <-c
// 打印计算结果
fmt.Println(x, y, x+y)
}
运行输出:
-5 17 12
这个例子通过channel将子协程中的计算结果回传给主协程,根据channel的特性,如果子协程的计算还没有完成,不会给channel发送数据,主协程读取channel的操作会一直阻塞,直到收到数据为止,这样就可以解决前面例子中,主协程退出,子协程未执行完就强制退出的问题。
4.缓冲channel
缓冲channel的创建如下:
// 给make函数,传递第2个参数,指定缓冲队列大小
ch := make(chan int, 100)
缓冲channel的使用方式跟无缓冲channel一样,区别就是往channel写输入数据的时候,如果缓存队列还没满,是不会阻塞写操作,例如:上面创建了的channel缓冲队列大小是100,如果写入到channel中,还未被取走的数据大于100,就会阻塞写操作。
5.遍历channel
可以使用for语句循环读取channel中的数据
例子:
package main
import (
"fmt"
)
// 定义计算斐波拉契数列的函数,通过channel参数c返回每一步的计算结果
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
// 返回当前计算结果
c <- x
x, y = y, x+y
}
// 通过close关闭channel
close(c)
}
func main() {
// 定义int类型的channel,缓冲队列大小是10
c := make(chan int, 10)
// 创建一个协程,开始计算数列
go fibonacci(10, c)
// 通过range关键词,循环遍历channel变量c,从c中读取数据
// 如果channel没有数据,就阻塞循环,直到channel中有数据
// 如果channel关闭,则退出循环
for i := range c {
fmt.Println(i)
}
}
运行输出:
0
1
1
2
3
5
8
13
21
34
6.select语句
select语句可以用来等待多个channel,直到其中一个channel可以读取到数据或者写入数据成功。
例子:
package main
import "fmt"
// 计算数列
func fibonacci(c, quit chan int) {
x, y := 0, 1
// 开始一个死循环
for {
// 通过select等待通道c和quit,看那个有反应,就执行对应的case语句中的代码
select {
case c <- x:
// 如果通道c写入数据成功,执行这里的计算逻辑
x, y = y, x+y
case <-quit:
// 如果收到通道quit的数据,就退出函数,结束计算
fmt.Println("quit")
return
}
}
}
func main() {
// 定义一个channel,用来接收计算结果
c := make(chan int)
// 定义一个channel,用来传递停止计算的通知
quit := make(chan int)
// 创建一个协程,用来打印计算结果
go func() {
// 打印10个计算结果
for i := 0; i < 10; i++ {
// 循环从c通道中读取10次数据
fmt.Println(<-c)
}
// 往quit通道中发送数据0,通知fibonacci函数退出计算,主协程就结束了
quit <- 0
}()
// 开始计算斐波拉契数列
fibonacci(c, quit)
}
运行输出:
0
1
1
2
3
5
8
13
21
34