Go的并发编程简述
简述了 Go 中的 goroutine,channel 和 WaitGroup,并通过例子来展示了这些功能的用法
Goroutine 简述
Go 对于异步编程提供了语言级别的支持,我们可以使用它的 goroutine 很方便地写出异步的代码。首先我们先通过一个简单的例子来认识 Go 的 goroutine。
在上面的代码中,我们启动了10个 goroutine 计算了10次{ 1 - 1000000000 }的和,主程序睡眠了10秒钟等待10个 goroutine 的结束。
goroutine 类似于 Python 中的协程。Go 语言自己实现了一个调度程序,负责调度 goroutine 的执行,每当 goroutine 程序遇到阻塞操作的时候,就把程序的控制权主动交还给调度程序,并保存自己的堆栈信息。Go 语言的调度程序拥有控制权后再来启动其他的 goroutine,其他的 goroutine 继续执行,当遇到阻塞操作后再把控制权交还给调度程序,以此往复,直到程序结束。
main 程序等待 goroutine 结束
在上面的代码中,我们的主程序是通过time.Sleep
来等待其他 goroutine 的结束,但是这样的方法很笨。我们需要在所有的 goroutine 运行完成之后,通知主程序退出,而不是让主程序傻傻地等一个固定时间。要实现这样的功能,我们可以使用channel
或者sync
包的WaitGroup
类型。
Channel
channel 是 Go 中的数据数据类型,可以被用来接收和发送数据,它具有一下特点:
- channel 必须要通过
make
函数创建 - channel 是带有是类型的,
ch := make(chan int)
表示声明一个 channel,其接收和发送的数据只能为int
类型。 - 管道可以有缓存,
ch := make(chan int, 20)
表示管道的缓存大小为20个int类型的数据。- 管道的缓存满了的时候,发送操作会阻塞
- 管道的缓存空的时候,接收操作会阻塞
- 如果整个程序所有的 goroutine 都是阻塞的,那么程序就会抛出异常,这样做是为了预防死锁
我们可以通过 channel 来改造我们上面的代码,创建一个 channel 在主程序和 goroutine 之间通信,当所有的 goroutine 都完成之后,再来通知主程序退出。改进后的程序代码如下:
从上述程序的输出结果中我们可以看到,程序的运行时间明显减短了,我们也不用担心某些没有运行完的 goroutine 因为主程序的退出而被强制结束。
WaitGroup
完成主程序和协程的同步工作,除了使用 channel 之外,我们还可以使用 Go 的sync.WaitGroup
类型,它可以让主程序阻塞地等待一组 goroutine 的结束,它的具体用法如下所示:
使用 select 等待多个 channel
上面的代码中,我们演示的都是一个 goroutine 中操作一个 channel,当我们需要在 goroutine 中同时操作多个 channel 时该怎么办呢?这就需要用到我们的select
语句,select
语句和switch
语句非常相似,只不过不同的是,select
判断的是一个 channel 是否是可读写的,而不是表达式的值。我们可以通过下面的代码来查看select
语句的用法:
上述代码简单地展示了如何利用select
语句来处理多个 channel,需要注意的是,case v, ok := <-c1
这条判断c1
是否已经关闭的语句可能会执行多遍,也就是说如果c1
关闭了,case v, ok := <-c1
还是永远可以读取出值来,且读取出来的ok
始终为false
。
如果我们不搞一个c1Close
来进行判断的话,那么o
中写入的两个值都可能是在判断c1
关闭的时候写入的。
ping-pong 示例代码
OK,看了上面那么多描述之后,我们可以看一个简单的例子,这个例子来自演讲 Advanced Go Concurrency Patterns
上述的代码也并不复杂,但是我觉得很有趣,就贴上来了。channel 可以认为是一个乒乓球桌,channel 中的数据就可以认为是一个乒乓球,每个 goroutine 接收到乒乓球以后,将球的击打次数加1,然后就将乒乓球扔回到乒乓球桌上,等待另一个 goroutine 来接收,如此往复。只有当主程序从桌子上拿走了乒乓球以后(主程序接收到了 channel 中的数据),两个协程才退出。