60

在查看 Go 和 Erlang 的并发方法时,我注意到它们都依赖于消息传递。

这种方法显然减轻了对复杂锁的需求,因为没有共享状态。

但是,考虑许多客户希望并行只读访问内存中单个大型数据结构(如后缀数组)的情况。

我的问题:

  • 使用共享状态是否会比消息传递更快并且使用更少的内存,因为锁大多是不必要的,因为数据是只读的,并且只需要存在于单个位置?

  • 在消息传递上下文中如何解决这个问题?会不会有一个进程可以访问数据结构,而客户端只需要按顺序从它请求数据吗?或者,如果可能的话,是否将数据分块以创建多个保存块的进程?

  • 鉴于现代 CPU 和内存的架构,这两种解决方案之间是否存在很大差异——即,共享内存是否可以由多个内核并行读取——这意味着没有硬件瓶颈会使两种实现大致相同?

4

10 回答 10

31

要意识到的一件事是,Erlang 并发模型并没有真正指定消息中的数据必须在进程之间复制,它指出发送消息是唯一的通信方式,并且没有共享状态。由于所有数据都是不可变的,这基本的,因此实现很可能不会复制数据,而只是发送对它的引用。或者可以使用两种方法的组合。与往常一样,没有最佳解决方案,在选择如何做时需要权衡取舍。

BEAM 使用复制,但发送引用的大型二进制文件除外。

于 2009-11-26T02:16:30.923 回答
28
  • 是的,在这种情况下,共享状态可能会更快。但前提是您可以放弃锁定,并且只有在绝对只读的情况下才可行。如果它“主要是只读的”,那么您需要一个锁(除非您设法编写无锁结构,否则请注意它们甚至比锁更棘手),然后您将很难让它执行为快速作为一个良好的消息传递架构。

  • 是的,您可以编写一个“服务器进程”来共享它。对于真正轻量级的流程,它并不比编写一个小的 API 来访问数据更繁重。像一个“拥有”数据的对象(在 OOP 意义上)一样思考。将数据分成块以增强并行性(在 DB 圈中称为“分片”)有助于在大情况下(或者如果数据存储速度较慢)。

  • 即使 NUMA 成为主流,每个 NUMA 单元仍然拥有越来越多的内核。一个很大的区别是,一条消息只能在两个内核之间传递,而一个锁必须从所有内核的缓存中刷新,将其限制为单元间总线延迟(甚至比 RAM 访问慢)。如果有的话,共享状态/锁变得越来越不可行。

简而言之.... 习惯于消息传递和服务器进程,它风靡一时。

编辑:重新审视这个答案,我想添加关于 Go 文档中的一个短语:

通过通信共享内存,不要通过共享内存进行通信。

这个想法是:当你在线程之间共享一块内存时,避免并发访问的典型方法是使用锁进行仲裁。Go 风格是通过引用传递消息,线程只有在收到消息时才访问内存。它依赖于某种程度的程序员纪律;但是会产生非常干净的代码,可以轻松校对,因此调试起来相对容易。

优点是您不必在每条消息上复制大块数据,也不必像某些锁定实现那样有效地刷新缓存。现在说这种风格是否会带来更高性能的设计还为时过早。(特别是因为当前的 Go 运行时在线程调度上有些幼稚)

于 2009-11-25T17:26:53.540 回答
15

在 Erlang 中,所有值都是不可变的 - 因此在进程之间发送消息时无需复制消息,因为无论如何都无法修改它。

在 Go 中,消息传递是约定俗成的 - 没有什么可以阻止您通过通道向某人发送指针,然后修改指向的数据,只有约定,所以再一次没有必要复制消息。

于 2009-11-26T16:15:03.133 回答
13

大多数现代处理器使用MESI 协议的变体。由于共享状态,在不同线程之间传递只读数据非常便宜。但是,修改后的共享数据非常昂贵,因为存储此缓存行的所有其他缓存都必须使其无效。

所以如果你有只读数据,在线程之间共享它而不是用消息复制是非常便宜的。如果您拥有以读取为主的数据,那么在线程之间共享可能会很昂贵,部分原因是需要同步访问,部分原因是写入会破坏共享数据的缓存友好行为。

不可变数据结构在这里可能是有益的。无需更改实际的数据结构,您只需创建一个共享大部分旧数据的新数据结构,但需要更改的内容已更改。共享单个版本很便宜,因为所有数据都是不可变的,但您仍然可以有效地更新到新版本。

于 2009-11-25T18:17:02.190 回答
5

什么是数据结构?

一个人大,另一个人小。

上周我和两个人谈过——一个人在制造嵌入式设备,他用了“大”这个词——我问他这是什么意思——他说超过 256 KB——同一周晚些时候,一个人在谈论媒体分发——他用“大”这个词我问他是什么意思——他想了想说“不适合一台机器”说 20-100 TBytes

在 Erlang 术语中,“大”可能意味着“不适合 RAM”——因此对于 4 GB 的 RAM 数据结构 > 100 MBytes 可能被认为是大的——复制 500 MBytes 的数据结构可能是一个问题。在 Erlang 中复制小型数据结构(比如 < 10 MBytes)从来都不是问题。

非常大的数据结构(即无法在一台机器上安装的数据结构)必须在多台机器上复制和“条带化”。

所以我猜你有以下几点:

小数据结构没问题——因为它们是小数据处理时间很快,复制也很快等等(只是因为它们很小)

大数据结构是一个问题——因为它们不适合一台机器——所以复制是必不可少的。

于 2009-11-30T15:07:29.463 回答
4

请注意,您的问题在技术上是无意义的,因为消息传递可以使用共享状态,所以我假设您的意思是通过深度复制进行消息传递以避免共享状态(正如 Erlang 目前所做的那样)。

使用共享状态是否会比消息传递更快并且使用更少的内存,因为锁大多是不必要的,因为数据是只读的,并且只需要存在于单个位置?

使用共享状态会快很多。

在消息传递上下文中如何解决这个问题?会不会有一个进程可以访问数据结构,而客户端只需要按顺序从它请求数据吗?或者,如果可能的话,是否将数据分块以创建多个保存块的进程?

可以使用任何一种方法。

鉴于现代 CPU 和内存的架构,这两种解决方案之间是否存在很大差异——即,共享内存是否可以由多个内核并行读取——这意味着没有硬件瓶颈会使两种实现大致相同?

复制对缓存不友好,因此会破坏多核的可扩展性,因为它会加剧对共享资源(即主存)的争用。

归根结底,Erlang 风格的消息传递是为并发编程而设计的,而您关于吞吐量性能的问题实际上是针对并行编程的。这是两个完全不同的主题,在实践中它们之间的重叠很小。具体来说,延迟通常与并发编程环境中的吞吐量一样重要,而 Erlang 风格的消息传递是实现理想延迟配置文件(即始终如一的低延迟)的好方法。共享内存的问题不是读写器之间的同步,而是低延迟的内存管理。

于 2011-02-10T21:26:31.527 回答
3

这里没有介绍的一种解决方案是主从复制。如果您有一个大型数据结构,您可以将更改复制到所有对其副本执行更新的从站。

如果一个人想要扩展到几台机器,这些机器甚至没有非常人为的设置(从远程计算机的内存读取/写入的块设备的 mmap?)

它的一个变体是有一个事务管理器,它很好地要求更新复制的数据结构,它会确保它同时服务一个且仅更新请求。这更像是用于 mnesia 表数据的主-主复制的 mnesia 模型,它符合“大数据结构”的条件。

于 2009-11-26T11:43:59.747 回答
3

目前的问题确实是锁定和缓存行一致性可能与复制更简单的数据结构(例如几百字节)一样昂贵。

大多数情况下,一个聪明的编写的新的多线程算法试图消除大部分锁定总是会更快 - 并且使用现代无锁数据结构更快。尤其是当您拥有精心设计的缓存系统时,例如 Sun 的 Niagara 芯片级多线程。

如果您的系统/问题不容易分解为几个简单的数据访问,那么您就有问题了。并不是所有的问题都可以通过消息传递来解决。这就是为什么仍然有一些基于 Itanium 的超级计算机出售的原因,因为它们具有 TB 的共享 RAM 和多达 128 个 CPU 在同一共享内存上工作。它们比具有相同 CPU 能力的主流 x86 集群贵一个数量级,但您不需要分解数据。

到目前为止没有提到的另一个原因是,当您使用多线程时,程序可以变得更容易编写和维护。消息传递和无共享方法使其更易于维护。

举个例子,Erlang 从来没有被设计成让事情变得更快,而是使用大量线程来构建复杂的数据和事件流。

我想这是设计的要点之一。在谷歌的网络世界中,你通常不关心性能——只要它可以在云中并行运行。理想情况下,通过消息传递,您可以在不更改源代码的情况下添加更多计算机。

于 2009-11-27T04:07:54.643 回答
1

通常消息传递语言(这在 erlang 中特别容易,因为它具有不可变的变量)优化进程之间的实际数据复制(当然仅限本地进程:您需要明智地考虑您的网络分布模式),所以这是不是什么大问题。

于 2009-11-30T02:47:07.870 回答
0

另一个并发范式是 STM,软件事务内存。Clojure 的 ref 得到了很多关注。Tim Bray 有一个很好的系列探索 erlang 和 clojure 的并发机制

http://www.tbray.org/ongoing/When/200x/2009/09/27/Concur-dot-next

http://www.tbray.org/ongoing/When/200x/2009/12/01/Clojure-Thes

于 2009-12-30T15:50:52.160 回答