让我们忘记将旧模式投入到新模式。在故事中有两个 Gopher,不再让一个 Gopher 从书堆一直运到焚化炉,而是在中间加入暂存区。因此,第一个 Gopher 将书籍搬运到暂存区,将它们丢下,跑回去再运另外一些。第二个 Gopher 坐在那里等待书达到暂存区,并把书从该位置搬运到焚化炉。如果一切良好,则有两个 Gopher 进程运行。它们是相同的类型,但有些细微不同,参数略有不同。如果系统将正常运行,一旦启动,其运行速度就会是原始模式的两倍。即使某些方面说它是完全不同的设计。一旦我们有了这个组合,我们可以采取另外的做法。
将以惯常的做法使其并行,同时运行整个程序的两个版本。翻倍之后,有了4个 Gopher,吞吐量将高达四倍。
或者,我们可以采用另一种做法,在刚才的并发多 Gopher 问题中,在中间加入暂存区。因此,现在我们有8个 Gopher 在运行,书籍非常快的速度被焚烧。
但这样还不够好,因为我们可以在另一个维度并行,运力全开。此时,有16个 Gopher 将这些书搬运到焚化炉中。显然,增加 Gopher 使问题解决的更好,是非常简单和愚蠢的。但我希望您了解,从概念上讲,这真的就是您考虑并行运行事物的方式。您无需考虑并行运行,而是考虑如何将问题以可分解、可理解、可操作的方式,分解为独立的块,然后组合起来以解决整个问题。
Lesson
以上就是的所有例子有什么意义呢?
首先,有很多方法可以做到这一点,我刚刚展示了一些。如果你坐在那里拿着一本速写册,你可能会想出另外50种让 Gopher 搬运书的方法。有很多不同的设计,它们不必都相同,但它们都能工作。然后,您可以对这些并发设计进行重构、重新排列、按不同的维度进行缩放,得到不同的功能以处理问题。真是太好了,因为不管你怎么做,处理这个问题的算法的正确性很容易保证。这样做不会搞砸,我的意思它们只是 Gopher,你知道这些设计本质上是安全的,因为你是那样做的。但是,这无疑是一个愚蠢的问题,与实际工作无关。嗯,事实上确实有关。
因为如果你拿到这个问题,把书堆换成一些网络内容;把 Gopher 换成 CPU,把推车换成网络或编码代码等等;把问题变成你需要移动数据;焚化炉是网络代理,或是浏览器,你想到的任何的数据使用者。您刚刚构建了一个 Web 服务体系结构的设计。你可能不认为你的 Web 服务架构是这样的,但事实上差不多就是这样。你可以把这两块替换掉看看,这正是你想的那种设计。当你谈论代理、转发代理和缓冲区之类会,扩容更多的实例的东西时,它们都在这个图上,只是不被这么想。本质上并不难理解它们,Gopher 能做到,我们也能。
A little background about Go
现在让我来展示如何在使用Go构建东西时采用这些概念。我不打算在这次演讲中教你 Go,希望你们有些人已经知道它,希望大家在之后能去更多了解它。但我要教一点点 Go,希望其他人也能像我们一样融入其中。
Goroutines
Go 有叫做 goroutine 的东西,可以认为有点像线程,但实际上是不同的。与其详细地谈有什么不同,不如说说它是什么吧。假设我们有一个函数,函数有两个参数。如果在程序中调用该函数 F,则在执行下一条语句之前要等待该函数完成。很熟悉,大家都知道。但是,如果在调用该函数之前放置关键字 go。你调用该函数,函数开始运行,虽然不一,至少在概念上可以立即继续运行。想想并发与并行,从概念上讲,当 F 不在时,程序一直在运行,你在做 F 的事情,不用等 F 回来。如果你觉得很困惑,那就把它想象成一个很像 shell 里的 & 符号。这就像在后台运行 F &,确切地说是一个 goroutine。
它有点像线程,因为一起运行在同一个地址空间中,至少在一个程序中如此。但 goroutine 要廉价得多,创建很容易,创建成本也很低。然后根据需要,goroutine 动态地多路映射到执行中的操作系统线程上,所以不必担心调度、阻塞等等,系统会帮你处理。当 goroutine 确实需要阻塞执行像 read 之类的系统调用时,其他 goroutine 不需要等待它,它们都是动态调度的。所以 goroutine 感觉像线程,却是更轻量版本的线程。这不是一个原始的概念,其他语言和系统已经实现了类似的东西。我们给它起了自己的名字来说明它是什么。所以称之为 goroutine。
Channels
刚刚已经提到需要在 goroutine 之间通信。为了做到这一点,在 Go 中,称之为 channel。它有点像 shell 中的管道,但它有类型,还有其他一些很棒的特性,今天就不深入了。但以下有一个相当小的例子。我们创建了一个timer channel
,显然它是一个时间值的 channel;然后在后台启动这个函数;sleep 一定的时间 deltaT,然后在 timer channel
上发送当时的时间 time.now()
。因为此函数是用 go 语句启动的,不需要等待它。它可以做任何想做的事情,当需要知道其他 goroutine 完成时,它说我想从 timer channel
接收那个值。该 goroutine 会阻塞直到有一个值被传递过来。一旦完成,它将设置为得到的时间,即其他 goroutine 完成的时间。小例子,但你需要的一切都在那张小幻灯片里。
Select
最后一部分叫做 select
。它让你可以通过同时监听多个 channel 控制程序的行为。一旦就能看出谁准备好通信了,你就可以读取。在这种情况下,channel 1
或 channel 2
,程序的行为将不同,取决于 channel 1
或 channel 2
是否准备就绪。在这种情况下,如果两个都没有准备好,那么 default
子句将运行,这意味着,如果没有人准备好进行通信,那么你会 fall through
。如果 default
子句不存在,执行 select
,需要等待其中一个或另一个 channel 就绪。如果它们都准备好了,系统会随机挑选一个。所以这种要晚一点才能结束。像 switch
语句,但用于通信场景。如果你知道 Dijkstra
的监督命令,应该会很熟悉
当我说 Go 支持并发,是说它确实支持并发,在 Go 程序中创建数千个 goroutine 是常规操作。我们曾经在会议现场调试一个go程序,它运行在生产环境,已经创建了130万个 goroutine,并且在调试它的时候,有1万个活跃的。当然,要做到如此,goroutine 必须比线程廉价得多,这是重点。goroutine 不是免费的,涉及到内存分配,但不多。它们根据需要增长和缩小,而且管理得很好。它们非常廉价,你可以认为和 Gopher 一样廉价。
Closures
你还需要闭包,我刚在前面的页面展示过闭包,这只是在 Go 语言中可以使用它的证据。因为它们是非常方便的并发表达式,可以创建匿名的 procedure
。因此,您可以创建一个函数,在本例中,组合多个函数返回一个函数。这只是一个有效的证明,它是真正的闭包,可以使用 go 语句运行。
让我们使用这些元素来构建一些示例。我希望你能潜移默化的学习一些 Go 并发编程,这是最好的学习方法。