21 并发

Golang使用Go协程(goroutine)实现并发,简单的说,它们就像线程。

Go协程独立执行,并共享内存空间。因为它们可以写操作相同的内存区域,所以多个协程在写全局变量时要尤其小心。

为了协调Go协程之间的工作,Go提供了数据通道(Channel),它本质上是线程安全的队列。

下面是一个使用Go协程池并利用数据通道(Channel)系协调它们工作的例子:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func sqrtWorker(chIn chan int, chOut chan int) {
	fmt.Printf("sqrtWorker started\n")
	for i := range chIn {
		sqrt := i * i
		chOut <- sqrt
	}
	fmt.Printf("sqrtWorker finished\n")
	wg.Done()
}

func main() {
	chIn := make(chan int)
	chOut := make(chan int)
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go sqrtWorker(chIn, chOut)
	}

	go func() {
		chIn <- 2
		chIn <- 4
		close(chIn)
	}()

	go func() {
		wg.Wait()
		close(chOut)
	}()

	for sqrt := range chOut {
		fmt.Printf("Got sqrt: %d\n", sqrt)
	}
}

// --------Output---------
sqrtWorker started
Got sqrt: 4
sqrtWorker started
sqrtWorker finished
Got sqrt: 16
sqrtWorker finished

这里有很多东西需要解释。

我们使用go sqrtWorker(chIn, chOut)启动了2个工作者协程。

每个sqrtWorker函数都是独立的、并发的运行。

我们使用一个可以传递整型数据的通道把要被工作者协程处理的数据排队,使用<-操作符即可发送数据到通道中。

在绝大多数时候,在多个协程中都访问(写)同一个变量是不安全的。使用Channelsync.WaitGroup则可以消除隐患。

工作者协程sqrtWorker中使用range从通道中拿数据。

我们并不能知道是哪个工作者协程拿了哪个特定的数据。当chInclose(chIn)关闭时,工作者协程中的for循环就会终止,接着协程也会执行结束。

为了把工作者协程中计算的结果返回给调用者,我们使用另一个数据通道。

在本例中,我们使用的是非缓冲通道,该类通道一次只能容纳一个元素。因此,我们启动了另一个协程去填充chIn,否则就会陷入死循环。

为了关闭工作者协程,故而关闭chIn

接着,我们通过迭代访问chOut取得工作者协程产出的结果。

还有个更复杂的问题,除非我们关闭了chOut,否则for sqrt := range chOut循环将会永远等待。

为了结束该循环,我们必须执行close(chOut),但是什么时机去做呢?

我们不能在sqrtWorker中去做,因为工作者协程是有很多个的,并且在已关闭的通道上再调用close会引发恐慌(panic)

sync.WaitGroup是一个线程安全的计数器,它可以被增加或减少,并且允许等待直到计数器的值为0。

在我们启动工作者协程之前,我们增加wg计数器的值wg.Add(1),在协程即将结束之前我们减少它的值wg.Done()

然后我们wg.Wait()等待该计数器的值归0,这意味着所有的工作者协程都结束了,可以安全关闭输出通道了。

为了避免阻塞main(),这必须在单独的协程中去wg.Wait()

为什么要费这么大力气去计算一个简单的值呢?

这只是个示例而已,在真实的场景中,一个协程会完成更多的工作,例如从互联网上下载文件或调整一个大图片的尺寸。

最后更新于