Advertisement
  1. Code
  2. Go

与Go同行:Golang的并发,第二篇

by
Read Time:8 minsLanguages:
This post is part of a series called Let's Go: Golang Concurrency.
Let's Go: Golang Concurrency, Part 1

Chinese (Simplified) (中文(简体)) translation by Qiang Ji (you can also view the original English article)

概述

Go独特功能之一是使用管道(channel)在goroutine之间进行通信。 在此教程中,你将会了解什么是管道(Channel),怎样有效地使用它们,和一些常见的模式。

什么是管道(Channel)?

管道是一个同步的驻在内存中的队列,goroutine和常规函数能使用此队列发送和接收带类型的值。 通信通过管道被序列化。

你使用make()创建一个管道并且指定此管道可以接受的值的类型。

ch := make(chan int)

Go为管道的发送和接收提供了一个优美的箭头语法:

你不必使用这个值。 只需从管道中拿到一个值就行。

<-ch

管道在默认的情况下是阻碍进程的。 如果你对管道发送一个值,你将阻碍进程直到有人从这个管道接收它。 同样,如果你从管道接收一个值,你将阻碍进程直到有人通过管道发送一个值。

以下程序演示了这一点。 在main()函数里定义一个管道,启动一个goroutine去打印一个“start”字符串,从管道里读取一个值,并且也把它打印出来。 然后main()函数启动另一个goroutine并每秒打印一个破折号("-")。 它休眠2.5秒后向管道发送一个值,并且再休眠3秒结束goroutine。

该程序非常好地展示了通道的阻碍性质。 第一个goroutine直接打印“start”字符串,但是当它试图从管道接收main()函数将休眠2.5秒后才发送的数值时,它被阻止。 另一个goroutine只是通过每隔一段时间定期打印破折号来提供时间流的视觉指示。

这里是程序输出:

缓存管道

这种行为将发送者与接收者紧密地耦合在一起,有时这不是你想要的。 Go提供了几个机制来解决这个问题。

缓存管道是一种能保存一定数量值的管道,这样发送者不会被阻碍直到缓存满了,即使没有接收者。

建立一个缓存管道只需要加一个管道容量作为管道生成函数的第二个参数。

ch := make(chan int, 5)

下面的程序说明了缓存管道的行为。 main()程序定义了一个有3个单位容量的缓存管道。然后它启动一个从管道缓存中读取数值的goroutine,每秒读一个并打印出来。另一个goroutine以每秒打印一个破折号的方式给你一个时间进程的视觉指示。 然后,它向管道发送5个值。

在程序运行时发生了什么呢? 前三个值立即通过管道被缓存,并且main()函数被阻碍。 一秒后,一个值通过goroutine被接收,并且main()函数能发送另一个值。 另一秒过去了,goroutine接收另一个值,并且main()函数能发送最后一个值。 在这时,goroutine保持每秒从管道接收数值。

这里是输出结果:

Select

缓存管道(只要缓存足够大)能够解决暂时波动的问题,就是当没有足够的接收者来接收所有被发出的信息。 但是反方向也有问题,就是被阻碍的接收者等待需要被处理的信息。 Go有办法帮你解决这些问题。

如果你希望你的goroutine在管道中没有要处理的消息的情况下执行其它操作,该怎么办? 一个好例子是你的接收者从多个管道中等待信息的到来。 你不想在管道A中被阻碍当管道B中正好有信息需要处理。 下面的程序试图用计算机的全部能力计算3和5的和。

这个主意模拟一个带冗余的复杂的操作(比如对一个分布式数据库进行一个远程的查询)。 sum()函数(注意它是怎样作为在main()内的内嵌函数被定义的)接受两个整数类型的参数并且返回一个整数类型的管道。 一个内部匿名goroutine随机休眠最多一秒然后对管道写入总和,关闭它,返回它。

现在main调用sum(3, 5)四次并且从变量ch1到ch4中保存结果管道。 四个对sum()的调用立即返回因为随机休眠在sum()函数被调用的goroutine里发生。

这里是最酷的部分。 select声明让main()函数等待所有的管道,并且对第一个返回的管道回应。 select声明的操作有点像switch声明的操作。

有时你不想main()函数因为等待第一个goroutine结束被阻碍。 在这种情况下,你可以添加一个默认情况,如果所有通道都被阻止,将执行此默认的操作。

一个网页爬虫的例子

在我之前的文章中,我在Go之旅中展示了一个网页爬虫练习的解决方案。 我使用了goroutine和一个同步的map. 我也通过管道解决了此练习。 两个解决方案的完整源代码可在GitHub看到。

让我们看一下相关的部分。 首先,无论什么时候goroutine解析一个页面,这是一个将会被发送进一个管道的struct。 它包含当前的网页内嵌深度和所有在此页面找到的URL。

fetchURL()函数接受一个URL, 一个深度值,和一个输出管道。 它使用提取器(在练习中已被提供)去提取在此页面上所有链接的URL。 它对做为一个links struct的参加者的管道发送一个作为单独信息的带一个递减深度的URL表单。 此深度代表我们应该让网页爬虫爬多远。 当深度到达0时,不会有更多的处理发生。

ChannelCrawl()函数协调所有的东西。 它跟踪所有的我们已经提取并保存在一个map中的URL。 不需要去同步访问因为没有其它函数或goroutine在运行。 它也定义了一个所有goroutine将会写入结果的参加者管道。

然后,它开始调用parseUrl作为处理每个新URL的goroutine。 此逻辑跟踪有多少goroutine通过管理一个计数器被启动。 无论什么时候一个数值从一个管道中被读取,计数器递减一位(因为发送goroutine在发送后退出),无论什么时候一个新的goroutine被启动,计数器递增一位。 如果深度到达零那么没有新的goroutine将会被启动,main函数将继续从管道中读取数值直到所有goroutine完成任务。

结论

Go管道为在goroutine之间安全通信提供了许多选择。 在语法上的支持既简洁又有说服力。 表达并发算法对程序员来说是一个真正的福音。 与我在这里介绍的相比,还有更多有关管道的知识。 我鼓励你深入了解它们所能实现的各种并发模式。

关注我们的公众号
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.