一、channel

管道(channel)是golang中用于多协程通信的手段,也是go编程中常用到的数据类型。

虽然被称为管道,但是并非在《unix环境高级编程》中说的管道(fifo和pipe),go中的管道实际上是一种通过共享内存来实现的多线程通信方式,只是名字叫做管道而已。和fifo和pipe这两者没有任何关系。

创建一个管道的方法:

msg := make(chan string, 3)

其中的chan string表明管道是string类型,后面的3是管道容量。创建管道时如果指定了管道容量,管道就是一个有缓冲的管道。也可以省略3表示当前是一个无缓冲的管道,一般不建议使用无缓冲管道,可能导致程序阻塞。有缓冲管道和无缓冲管道的区别:

  1. 无缓冲管道:管道中没有元素的时候,读端会阻塞。管道有元素没有被读出的时候,写端阻塞。
  2. 有缓冲管道:管道中没有元素的嘶吼,读端会阻塞。管道中元素满了,写端阻塞。

管道的读写操作方法:

// 写管道
msg <- "HelloWorld"
// 读管道
val <- msg

示例

以下是一个无缓冲chan的执行示例,代码中分别创建两个goroutine,一个从管道读,一个往管道写,写端是在休眠一秒后才写。两个goroutine通过WaitGroup来同步结束状态:

func main() {
    var wg sync.WaitGroup
    msg := make(chan string)

    wg.Add(2)
    go func() {
        glog.Info("wait to msg...")
        glog.Info("receive msg: ", <-msg)
        wg.Done()
    }()

    go func() {
        time.Sleep(time.Second)
        msg <- "HelloWorld"
        wg.Done()
    }()

    wg.Wait()
}
代码中的输出使用glog来打印,方便输出更多信息。

输出结果:

管道接收方一共打印了两条日志,一条是协程刚启动准备接受消息时候的日志,一条是收到消息后的日志。对比两条日志的打印时间能看到中间间隔了1秒,这一秒刚好是管道发送方在睡眠,说明接收方在这一秒是在等待发送方发送数据,没有数据的时候被阻塞了。

使用无缓冲区管道千万要注意的就是阻塞,如果处理不当,读端没有及时读或者读端挂了,很可能就导致业务阻塞。

二、基于管道的异步调用

高并发场景中,一种经常用到的处理操作是异步调用,如何通过管道来实现异步调用呢?

以下是一段基于管道实现的异步调用示例,代码模拟了一次异步任务处理过程:主线程需要处理一个任务(耗时1秒),同时还要从数据库中读取一个字符串的数据。

func main() {
    var wg sync.WaitGroup
    msg := make(chan string)

    wg.Add(1)

    go func() { // 创建新的协程从数据库获取字符串
        glog.Info("service start!")
        msg <- "HelloWorld"
        glog.Info("service done!")
        wg.Done()
    }()

    glog.Info("doJob...")
    time.Sleep(time.Second) // 主线程执行任务

    glog.Info("service return: ", <-msg)
    wg.Wait()
}

程序运行结果:

从四条日志的打印时间来看,程序一共执行了大约1秒钟的时间,doJobservice start!几乎同时输出,两者在同时执行。过了一秒钟后,主函数执行完成,再从管道获取字符串数据,这样就省去了一次获取字符串数据的逻辑。假设获取字符串需要0.5秒,就节省了0.5秒的时间。

如果不使用这个异步的操作,那么整个函数的流程应该是:

  1. 主函数执行任务(1秒)
  2. 执行完成后再获取字符串数据(0.5秒)
  3. 函数结束(总耗时1.5秒)

使用异步之后,效率明显变高了。

三、有缓冲的管道

有缓冲的管道和无缓冲的管道最大的区别是往有缓冲管道内些数据时,如果管道还存在空间,写操作就不会阻塞,而无缓冲管道只要没有读端读出管道内数据,就会一直阻塞。

从上面的示例来看,msg是一个无缓冲区的管道,因此在创建的goroutine中写入数据后被阻塞,直到1秒后才打印出server done!。对这个协程而言,这1秒钟的过程无需阻塞,因为它的任务就是提取一个字符串消息然后放到管道,把数据放到管道后它的任务就完成了,可以直接结束了。没必要等到主函数读完才结束,否则很多协程都这样阻塞,纯粹浪费系统资源,降低性能。

那么这里就可以使用有缓冲区的管道来改进这一点,给管道设置一个缓冲区,新的协程往里面写数据就不会阻塞了,写完就能退出。代码中只需要修改msg创建时候的大小就可以了:

msg := make(chan string, 1)

执行结果:

可以看到,新协程启动和结束都是非常快速地,并没有等到一秒后才退出。同时主函数也在一秒后读到了字符串。

最后修改:2020 年 02 月 23 日
如果觉得我的文章对你有用,请随意赞赏