Concurrency vs Parallelism
如果你看看今天的编程语言,可能会发现这个世界是面向对象的,但实际上并非如此,世界是并行的。你通过网络等等,从最底层(例如:多核计算机)获取所有的东西,一路攀升到了解星球、宇宙。世界上所有的东西都在同时发生,但我们拥有的计算工具实际上并不擅长表达这种世界观。看起来似乎是一种失败,如果我们了解什么是并发性,以及如何使用它,就可以解决这个问题。
我将假设你们中的大多数人至少听说过 Go 编程语言,它也是我最近几年在 Google 工作的内容。Go 是一种并发语言,也就是说它使并发性有用,有像上帝一样同时执行事物的能力;有在同时执行的事物之间进行通信的能力;有叫做 select
语句的东西,它是一个多路并发控制开关。如果你搞不懂是怎么回事,不用担心。
在大约两年前,当我们发布 Go 时,在场的程序员都说:”哦,并发工具,我知道做什么的,并行运行,耶“。但实际并非如此,并发性和并行性是不一样的,这是个常被误解的问题。我在这里试图解释原因,并向您展示并发性实际上更好。那些困惑的人会碰到什么事情:他们执行的程序,在更多的处理器上会变得更慢。他们会认为有问题,不管用,想要逃开。但真正有问题的是世界观,我希望我能改正它。
什么是并发性?并发性,如我当前使用的一样,用于计算机科学领域,是一种构建事物的方法。它是由独立执行的事物组成,通常是 function
,虽然不一定必须如此。我们通常称之为交互式进程。称其为进程,并不是指 Linux 进程,指的是一种普遍的概念,它包括线程、协程、进程等等,所以尽可能抽象的理解。并发性由独立执行的进程组成;
另一方面,并行性是同时执行多个事物,可能相关,也可能无关。
如果你用语焉不详的方式思考,并发性是指同时负责很多事情;并行性是指同时做很多事情。它们显然是相关的,但实际上是不同的概念,如果没有合适的工具包,尝试思考它们会有些困惑。一个是关于结构并发性,另一个是关于执行并行性。我会告诉你为什么这些概念很重要。并发性是一种构造事物的方法,以便可以使用并行性更好地完成工作。但并行性不是并发性的目标,并发性的目标是好的结构。
An analogy
如果你在运行一个操作系统的话,会很熟悉的一个类比。操作系统可能有鼠标驱动程序、键盘驱动程序、显示驱动程序、网络驱动程序等等,都是由操作系统管理的,内核中的独立事物。它们都是并发的事物,却不一定是并行的。如果只有一个处理器,同一时间其中只有一个处于运行。I/O 设备具有一个并发模型,但本质不是并行的,它不需要是并行的。并行的事物可能类似向量点积,可以分解为微观操作,以使得可以在一些精美的计算机上并行执行。非常不同的概念,完全不是同一个东西。
Cocurrency plus communication
为了能利用并发性,必须添加 communication
的概念。我今天不会聚焦于该概念太多,但一会儿你会看到一点点。并发性提供了一种将程序构造为独立块的方法,然后,必须使这些块协调一致。要使之工作,需要某种形式的 communication
。Tony Hoare
在1978年写了一篇论文叫做 《communicating sequential processes》
,实在是计算机科学中最伟大的论文之一。如果你还没读过,要是从本次演讲中真有什么领悟的话,回家你应该读读那篇论文。它太不可思议了,基于此论文,很多人未进行太多考虑就遵循、并构建工具,以将其思想运用到并发语言中,比如另一种很棒的语言Erlang。GO 中也有其一些思想,关键点都在原稿中,除了稍后会提到的几个小例外。
Gophers
但,你看这一切太抽象了。我们需要 Gopher 的帮忙,来一些 Gopher。
有一个真正的问题我们要解决。有一堆过时的手册,可以是 C++98 手册,现在已经是 C++11;或许是 C++11 书籍,但不再需要了。关键是我们想要清理掉它们,它们占用了大量空间。所以我们的 Gopher 有一个任务,把书从书堆里取出来,放到焚化炉里清理掉。但是,如果是一大堆书,只有一个 Gopher 需要很长时间。Gopher 也不擅长搬运书籍,尽管我们提供了小车。
所以再增加一个 Gopher 来解决这个问题,只有 Gopher 不会好起来,对吧?
因为它需要工具,无可厚非,我们需要提供所有它需要的东西。Gopher 不仅需要作为 Gopher 的能力,也需要工具来完成任务。再给它一辆车,现在这样应该会更快。在两个 Gopher 推车的情况下,肯定能更快地搬运书。但可能存在一个小问题,因为我们必须使它们同步。来回奔波中,书堆互相妨碍,它们可能会被困在焚化炉里,所以它们需要协调一点。所以你可以想象 Gopher 们发送 Tony Hoare
的短信息,说:我到了,我需要空间把书放进焚化炉。不管是什么,但你明白了,这很傻。但我想解释清楚,这些概念并不深刻,它们非常好。
如何让它们搬运得更快,我们把一切都增加一倍。我们提供两个 Gopher,把书堆,焚化炉和 Gopher 一样也增加一倍。现在我们可以在相同的时间里搬运两倍的书,这是并行,对吧?
但是,想象它不是并行,而是两个 Gopher 的并发组合。并发性是我们表达问题的方式,两个 Gopher 可以做到这一点。我们通过实例化 Gopher 程序的更多实例来并行,这被称为进程(在此情况下称为 Gopher)的并发组合。
现在这种设计不是自动并行的。确实有两个 Gopher,但是谁说它们必须同时工作呢?我可以说,同时只有一个 Gopher 可以移动,就像单核计算机,此设计仍然是并发的,很好很正确,但它本质上不是并行的,除非让两个 Gopher 同时搬运。当并行出现时,有两个事物同时执行,而不仅仅是拥有两个事物。这是一个非常重要的模型,一旦断定理解了它,我们就会明白可以把问题分解成并发的块。
我们可以想出其他模型,下面有一个不同的设计。在图中有三个 Gopher,同一堆书,同一个焚化炉,但是现在有三个 Gopher。有一个 Gopher,它的工作就是装车;有一个 Gopher,它的工作就是推车,然后再把空的还回去;还有一个 Gopher,它的工作就是装入焚化炉。三个 Gopher,速度理应会更快。但可能不会快多少,因为它们会被阻塞。书可能在错误的地方,在那里没有什么需要用车来做的。
让我们处理下这个问题,另外增加一个Gopher归还空车,这明显很傻。但我想指出一个相当深刻的问题,这个版本的问题实际上会比之前版本的问题执行得更好。尽管为了完成更多的工作,增加了一个 Gopher 来回奔波。因此,一旦我们理解了并发性的概念,就可以向图片增加 Gopher,真的可以做更多的工作,使之运行得更快。因为管理的更好的块的并发组合真的可以运行得更快。工作可能不会恰好完美地进行,但是可以想象如果所有的 Gopher 的时间都恰到好处,它们知道每次搬运多少书。并发组合真的可以让4个 Gopher 都一直在忙碌。事实上,此版本可能比原来的版本快四倍。虽然可能性不大,但是我想让你理解,是可能的。
此时有一个发现,它非常重要而且很微妙,有些柔性。我们在现有的设计中通过添加并发程序来提高程序的性能。我们真的添加了更多的东西,整个过程变得更快了。如果仔细想想,有点奇怪,也有点不奇怪。因为额外添加了一个 Gopher,而且 Gopher 确实工作。但是如果你忘记了它是个 Gopher 的事实,并认为只是增加了东西,设计确实可以使它更高效。并行性可以出自于对问题更好的并发表达,这是一个相当深刻的见解。因为 Gopher 们的参与所以看起来不像。但是没关系。
此时有四个进程并发运行。一个 Gopher 将东西装到车中;一个 Gopher 把车运到焚化炉;还有另一个 Gopher 将车中的物品卸到焚化炉中;第四个 Gopher 把空车还回来。您可以将它们视为独立的进程,完全独立运行的进程,我们只是将它们并行组合以构建完整的程序解决方案。这不是我们唯一可以构造的方案,以下是一个完全不同的设计。
通过增加另外一个堆书、焚化炉、和4个 Gopher,可以使该设计更加并行。但关键是,采用已有概念以分解问题。一旦清楚这是并发分解,就可以在不同的纬度上使其并行化。无论能否获得更好的吞吐量,但是至少,我们得以更细粒度的理解问题,可以控制这些块。在此情况下,如果一切刚好,会有8个 Gopher 努力以烧掉那些C++手册。
当然,也许根本没有发生并行,谁说这些 Gopher 必须同时工作,我可能每次只能运行一个 Gopher。在这种情况下,该设计只能像原始问题一样,以单个 Gopher 的速率运行。它执行时,其他7个将全部空闲。但该设计仍然是正确的。这很了不得,因为意味着我们在保证并发性时不必担心并行性。如果并发性正确,并行性实际上是一个自由变量,决定有多少个 Gopher 处于忙碌。我们可以为整个事情做一个完全不同的设计。