Go 协程通信实现(下)—— 通过 channel 进行消息传递


上篇教程学院君演示了如何通过共享内存实现协程通信,不过这种方式太过繁琐,且维护成本高,Go 语言推荐使用消息传递实现并发通信,这种消息通信机制被称为 channel,中文译作「通道」,可理解为传递消息的通道。

通道是 Go 语言在语言级别提供的协程通信方式,它是一种数据类型,本身是并发安全的,我们可以使用它在多个 goroutine 之间传递消息,而不必担心通道中的值被污染。

注:需要注意的是,通道是进程内的通信方式,因此通过通道传递对象的过程和调用函数时的参数传递行为一致,也可以传递指针。如果需要跨进程通信,建议通过分布式系统的方法来解决,比如使用 Socket 或者 HTTP 等通信协议,Go 语言对于网络方面也有非常完善的支持,学院君会在介绍完并发编程后介绍网络通信。

前面我们说到通道是一种数据类型,和数组/切片类型类似,一个通道只能传递一种类型的值,这个类型需要在声明 通道时指定。在使用通道时,需要通过 make 进行声明,通道对应的类型关键字是 chan

ch := make(chan int)

这里我们初始化了一个通道类型 ch,其中只能传递 int 类型的值。

我们可以把通道看作是一个先进先出(FIFO)的队列,通道中的元素会严格按照发送顺序排列,继而按照排列顺序被接收,通道元素的发送和接收都可以通过 <- 操作符来实现,发送时元素值在右,通道变量在左:

ch <- 1  // 表示把元素 1 发送到通道 ch

接收时通道变量在右,可以通过指定变量接收元素值:

element := <-ch

也可以留空表示忽略:

<-ch

这样一来,通过箭头指向我们就可以清楚的判断是写入数据到通道还是从通道读取数据,非常简单形象。

在系统介绍通道类型的完整语法前,我们先看通过通道类型重写上篇教程通过共享内存实现协程通信的代码,创建一个 channel.go 文件,编写代码如下:

package main

import (
    "fmt"
    "time"
)

func add(a, b int, ch chan int) {
    c := a + b
    fmt.Printf("%d + %d = %d\n", a, b, c)
    ch <- 1
}


func main() {
    start := time.Now()
    chs := make([]chan int, 10)
    for i := 0; i < 10; i++ {
        chs[i] = make(chan int)
        go add(1, i, chs[i])
    }
    for _, ch := range chs {
        <- ch
    }
    end := time.Now()
    consume := end.Sub(start).Seconds()
    fmt.Println("程序执行耗时(s):", consume)
}

在这个例子中,我们首先定义了一个包含 10 个通道类型的切片 chs,并把切片中的每个通道分配给 10 个不同的协程。在每个协程的 add() 函数业务逻辑完成后,我们通过 ch <- 1 语句向对应的通道中发送一个数据。在所有的协程启动完成后,我们再通过 <-ch 语句从通道切片 chs 中依次接收数据(不对结果做任何处理,相当于写入通道的数据只是个标识而已,表示这个通道所属的协程逻辑执行完毕),直到所有通道数据接收完毕,然后打印主程序耗时并退出。

之所以上述这段代码可以实现和「共享内存+锁」一样的效果,是因为往通道写入数据和从通道接收数据都是原子操作,或者说是同步阻塞的,当我们向某个通道写入数据时,就相当于该通道被加锁,直到写入操作完成才能执行从该通道读取数据的操作,反过来,当我们从某个通道读取数据时,其他协程也不能操作该通道,直到读取完成,如果通道中没有数据,则会阻塞在这里,直到通道被写入数据。因此,可以看到通道的发送和接收操作是互斥的,同一时间同一个进程内的所有协程对某个通道只能执行发送或接收操作,两者不可能同时进行,这样就保证了并发的安全性,数据不可能被污染。

综上可知,上述示例代码 main() 函数中的第二个循环会等到所有子协程执行完毕后才能完成所有通道的接收操作。我们可以执行下这段代码,输出结果如下:

程序耗时和共享内存相当,但是代码要简洁的多,优雅的多。当然,本篇教程只是介绍了通道的最基本使用,接下来,我将花几个教程的篇幅为大家系统介绍通道,并结合协程(goroutine)+通道(channel)为大家展现 Go 并发编程的魅力。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: Go 协程通信实现(上)—— 共享内存

>> 下一篇: 通道类型篇(一):基本语法和缓冲通道