一架梯子,一头程序猿,仰望星空!

go语言协程(goroutine)


协程是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