Some examples
Launching daemons
从启动一个守护进程开始,您可以使用闭包来包装一些您希望完成但不想等待的后台操作。在这种情况下,我们有两个 channel 输入和输出,无论出于什么原因,我们需要将输入传递到输出,但不想等到复制完成。所以我们使用 go func 和 闭包,然后有一个 for 循环,它读取输入值并写入输出,Go 中的 for range 子句将耗尽 channel。它一直运行直到 channel 为空,然后退出。所以这一小段代码会自动耗尽 channel。因为在后台运行,所以你不需要等待它。这是一个小小的范例,但你知道它还不错,而且已经习惯了。
A simple load balancer
现在让我向您展示一个非常简单的 Load Balancer
。如果有时间的话,我会给你看另一个例子。这个例子很简单,想象一下你有一大堆工作要完成。我们将它们抽象出来,将它们具体化为一个包含三个整数值的 Work
结构体,您需要对其执行一些操作。
worker
要做的是根据这些值执行一些计算。然后我在此处加入 Sleep
,以保证我们考虑阻塞。因为 worker
可能会被阻塞的一定的时间。我们构造它的方式是让 worker
从 input channel 读取要做的工作,并通过 output channel 传递结果,它们是这个函数的参数。在循环中,遍历输入值,执行计算,sleep 一段任意长的时间,然后将响应传递给输出,传递给等待的任务,所以我们得操心阻塞。那一定很难,对吧,以下就是全部的解决方案。
之所以如此简单,是因为channel 以及它与语言的其他元素一起工作的方式,让您能够表达并发的东西,并很好地组合它们。做法是创建两个 channel, input channel 和 output channel,连接到 worker
。 所有 worker
从 input channel 读取,然后传输到 output channel;然后启动任意数量的 worker
。注意中间的 go 子句,所有 worker
都在并发运行,也许是并行运行;然后你开始另一项工作,如屏幕显示为这些 worker
创造大量的工作,然后在函数调用中挂起,接收大量的结果,即从 ouput channel 中按照结果完成的顺序读取其值。因为作业结构化的方式,不管是在一个处理器上运行还是在一千个处理器上运行,都会正确而完整地运行。任何人使用该资源都可以同样完成,系统已经为你做好了一切。如果你思考这个问题,它很微不足道。但实际上,在大多数语言中,如果没有并发性,很难简洁地编写。并发性使得做这种事情,可以非常紧凑。
更重要的是,它是隐式并行性的(尽管不是,如果你不想,可以不必考虑该问题),它也能很好地扩展。没有同步或不同步。worker
数量可能是一个巨大的数字,而且它仍然可以高效地工作,因此并发工具使得为较大问题构建此类解决方案变得很容易。
还要注意,没有锁了,没有互斥锁了,所有这些都是在考虑旧的并发模型时需要考虑的,新模型没有了,你看不到它们的存在。然而,一个正确的无锁的并发、并行算法,一定很好,对吧?
但这太容易了,我们有时间看一个更难的。
Load balancer
此例子有点棘手,相同的基本概念,但做的事情更符合现实。假设我们要写一个 Loader Balancer
,有一堆 Requester
生成实际的工作,有一堆工作任务。希望将所有这些 Requester
的工作负载分配给任意数量的 Worker
,并使其达到某种负载平衡,所以工作会分配给负荷最少的Worker
。 所以你可以认为 Worker
们一次可能有大量的工作要做。他们可能同时要做的不止一个,可能有很多。因为有很多请求在处理,所以这会是一个很忙碌的系统。正如我演示的一样,它们也许是在同一台机器上。您也可以想象,其中一些线代表了正在进行适当负载均衡的网络连接,从结构上讲,我们的设计仍然是安全的。
Request
现在看起来很不一样了。有一个任意数量函数的闭包,表示我们要做的计算;有一个 channel 可以返回结果。请注意,不像其他一些类似 Erlang 的语言,在 Go 中 channel 是 Reuqest
的一部分,channel 的概念就在那里,它是语言中 first-class
的东西,使得可以到处传递 channel。它在某种意义上类似于文件描述符,持有 channel 的对象就可以和其他对象通信,但没有 channel 的对象是做不到的。就好像打电话给别人,或者通过文件描述符传递文件一样,是一个相当有影响的概念。想法是,要发送一个需要计算的请求,它包含一个计算完成返回结果的 channel。
以下是一个虚构但能起到说明作用的版本的 Requester
。所做的是,有一个请求可以进入的 channel,在这个 work channel
上生成要做的要做的任务;创建了一个 channel,并放入每个请求的内部,以便返回给我们答案。做了一些工作,使用 Sleep
代表(谁知道实际上在做什么)。你在 work channel
上发送一个带有用于计算的函数的请求对象,不管是什么,我不在乎;还有一个把答案发回去的 channel;然后你在那个 channel 等待结果返回。一旦你得到结果,你可能得对结果做些什么。这段代码只是按照一定速度生成工作。它只是产生结果,但是通过使用 input 和 output channel 通信来实现的。
然后是 Worker
,在前面的页面,记得么?有一些 Requester
,右边的是Worker
,它被提供给 balancer,是我最后要给你看的。Worker
拥有一个接收请求的 channel;一个等待任务的计数,Worker
拥有任务的数量代表其负载,它注定很忙;然后是一个 index,是堆结构的一部分,我们马上展示给你看。Worker
所做的就是从它的 Requester
那里接收工作。Request
channel 是 Worker
对象的一部分。
调用 Worker
的函数,把请求传递给它,把从 Requester
生成的实际的函数通过均衡器传递给 Worker
。Worker
计算答案,然后在 channel 上返回答案。请注意,与许多其他负载均衡架构不同,从 Worker
返回给 Requester
的 channel 不通过 Loader Balancer
。一旦 Requester
和 Worker
建立连接,图中的“中介”就会消失,请求上的工作直接进行通信。因为在系统运行时,系统内部可以来回传递 channel。如果愿意,也可以在里面放一个 goroutine
,在这里放一个 go 语句,然后在 Worker
上并行地处理所有的请求。如果这样做的话,一样会工作的很好,但这已经足够了。
Balancer
有点神奇,你需要一个 Worker
的 pool
; 需要一些 Balancer
对象,以绑定一些方法到 Balancer
。Balancer
包含一个 pool
;一个 done channel
,用以让 Worker
告诉 Loader Balancer
它已经完成了最近的计算。
所以 balance
很简单,它所做的只是永远执行一个 select
语句,等待做更多来自 Requester
的工作。在这种情况下,它会分发请求给负载最轻的 Worker
;或者 Worker
告知,它已经完成计算,在这种情况下,可以通过更新数据结构表明 Worker
完成了它的任务。所以这只是一个简单的两路 select
。然后,我们需要增加这两个函数,而要做到这一点,实际上要做的就是构造一个堆。
我跳过这些令人很兴奋的片段,你们已经知道什么意思。
Dispatch
, dispatch
要做的就是找到负载最少的 Worker
,它是基于堆实现的一个标准优先级队列。所以你把负载最少的 Worker
从堆里取出来,通过将请求写入 request channel 来发送任务。因为增加了一个任务,需要增加负载,这会影响负载分布。然后你把它放回堆的同一个地方,就这样。你刚刚调度了它,并且在结构上进行了更新,这就是可执行代码行的内容。
然后是 complete
的任务,也就是工作完成后,必须做一些相反的事情。 Worker
的队列中减少了一个任务,所以减少了它的等待计数。从堆里弹出 Worker
,然后把它放回堆中,优先级队列会把它放回中它所属的位置,这是一个半现实的 Loader Balancer
的完整实现。此处的关键点是数据结构使用的是 channel 和 goroutine 来构造并发的东西。
Lesson
结果是可伸缩的,是正确的,很简单,没有显式的锁,而架构让它得以实现。因此,并发性使此例子的内在并行性成为可能。你可以运行这个程序,我有这个程序,它是可编译、可运行的,而且工作正常,负载均衡也做得很好。物体保持在均匀的负载下,按照模块量化,很不错。我从来没说过有多少 Worker
,有多少问题。可能每个都有一个,另一个有数10个;或者每个都有一千,或者每个都有一百万,扩缩容仍然有效,并且仍然高效。
One more example
再举一个例子,这个例子有点令人惊讶,但它适合一张幻灯片就可以完成。
想象一下如何复制数据库,你得到了几个数据库,每个数据库中有相同的数据,谷歌称之为分片,称呼相同的实例。您要做的是向所有数据库传递一个请求,一个查询,并返回结果。结果会是一样的,你选择第一个应答请求来加快速度,因为首先要回来的是你想要的答案。如果其中一个坏了,断开了或者什么的,你不在乎。因为会有其他响应回来,这就是如何做到这一点。这就是它的全部实现。您有一些连接数组和一些要执行的查询,您创建一个 channel,该 channel 缓冲查询数据库中的元素数、副本内的副本数大小的内容,然后您只需在数据库的所有连接上执行。对于其中的每一个,您启动一个 goroutine 以将查询传递到该数据库,然后获取答案。但是通过这个 DoQuery 调用,将答案传递到唯一的 channel,这个 channel 保存所有请求的结果。然后,在你执行之后,所有的 goroutine 都只需在底部这行等待。我们等待第一个回到 channel 的请求,就是你想要的答案。返回它,就完成了。这看起来像个玩具,而且有点像。但这实际上是一个完全正确的实现,唯一缺少的是干净的回收。你想告诉那些还没回来的服务器关闭。当你已经得到答案,不再需要它们。你可以做,增加更多且合理的代码,但那就不适合放在幻灯片上了。所以我只想告诉你,在很多系统中,这是一个相当复杂的问题,但在这里,它只是自然地脱离了架构。因为你已经有了并发工具来表示一个相当大的复杂的分布式问题,它运行得非常好。