在研究Go和Erlang的并发方法时,我注意到它们都依赖于消息传递。
这种方法显然减轻了对复杂锁的需求,因为没有共享状态。
但是,考虑许多客户端希望对内存中的单个大型数据结构进行并行只读访问的情况 - 比如后缀数组。
我的问题:
使用共享状态会比消息传递更快并且使用更少的内存,因为锁是大多数不必要的,因为数据是只读的,只需要存在于一个位置吗?
如何在传递上下文的消息中处理此问题?是否存在可以访问数据结构的单个进程,客户端只需要从中顺序请求数据?或者,如果可能的话,数据是否会被分块以创建几个保存块的进程?
鉴于现代CPU的架构和内存,这两个解决方案之间有很大差异 - 也就是说,共享内存可以由多个内核并行读取 - 这意味着没有硬件瓶颈可能会使两个实现大致相同吗?
答案 0 :(得分:28)
要认识到的一件事是Erlang并发模型 NOT 确实指定消息中的数据必须在进程之间复制,它指出发送消息是唯一的通信方式,并且有没有共享状态。由于所有数据都是不可变的, 基础,因此实现可能不会复制数据,只是发送对它的引用。或者可以使用两种方法的组合。与往常一样,没有最佳解决方案,在选择如何做时需要权衡利弊。
BEAM使用复制,但发送引用的大型二进制文件除外。
答案 1 :(得分:26)
是的,在这种情况下,共享状态可能会更快。但只有你可以放弃锁,这只有在绝对只读时才可行。如果它“大部分是只读”那么你需要一个锁(除非你设法写无锁结构,警告他们甚至比锁更棘手),然后你很难让它像快速作为一种良好的消息传递架构。
是的,您可以编写一个“服务器进程”来共享它。使用非常轻量级的流程,与编写小型API来访问数据相比,它并不重要。想象一个'拥有'数据的对象(在OOP意义上)。以块为单位拆分数据以增强并行性(在数据库圈中称为“分片”)有助于处理大型情况(或者如果数据处于缓慢存储状态)。
即使NUMA成为主流,每个NUMA单元仍然拥有越来越多的内核。一个很大的区别是消息可以在两个内核之间传递,而锁必须从所有内核的缓存中刷新,将其限制为单元间总线延迟(甚至比RAM访问慢)。如果有的话,共享状态/锁定变得越来越不可行。
简而言之......习惯了消息传递和服务器进程,这一切都风靡一时。
编辑:重温这个答案,我想补充一下Go的文档中找到的短语:
通过通信共享内存,不通过共享内存进行通信。
这个想法是:当线程之间共享一块内存时,避免并发访问的典型方法是使用锁来进行仲裁。 Go样式是使用引用传递消息,线程仅在接收消息时访问内存。它依赖于一些程序员纪律;但是会产生非常干净的代码,可以很容易地进行校对,因此调试相对容易。
优点是您不必在每条消息上复制大块数据,也不必像某些锁实现那样有效地清除缓存。如果风格能够带来更高性能的设计,那还为时尚早。 (特别是因为当前的Go运行时在线程调度上有些天真)
答案 2 :(得分:12)
在Erlang中,所有值都是不可变的 - 因此在进程之间发送消息时无需复制消息,因为它无论如何都无法修改。
在Go中,消息传递是按照惯例进行的 - 没有什么可以阻止你向某人发送一个指针通过一个通道,然后修改指向的数据,只有约定,所以再一次没有必要复制消息。
答案 3 :(得分:11)
大多数现代处理器使用MESI protocol的变体。由于共享状态,在不同线程之间传递只读数据非常便宜。但是,修改后的共享数据非常昂贵,因为存储此缓存行的所有其他缓存必须使其无效。
因此,如果您拥有只读数据,则在线程之间共享它而不是使用消息进行复制非常便宜。如果你有读取主要数据,在线程之间共享可能会很昂贵,部分原因是需要同步访问,部分原因是写入破坏了共享数据的缓存友好行为。
Immutable data structures在这里很有用。您只需创建一个共享大部分旧数据的新数据,而不是更改实际的数据结构,但需要更改您需要更改的内容。共享它的单个版本很便宜,因为所有数据都是不可变的,但您仍然可以有效地更新到新版本。
答案 4 :(得分:4)
请注意,您的问题在技术上是非敏感的,因为消息传递可以使用共享状态,因此我将假设您的意思是使用深度复制传递消息以避免共享状态(如Erlang目前所做的那样)。
使用共享状态是否会比消息传递更快并且使用更少的内存,因为锁是大多数不必要的,因为数据是只读的,只需要存在于一个位置?
使用共享状态将更快批次。
如何在传递上下文的消息中处理此问题?是否存在可以访问数据结构的单个进程,客户端只需要从中顺序请求数据?或者,如果可能的话,数据是否会被分块以创建几个保存块的进程?
可以使用任何一种方法。
鉴于现代CPU的架构和内存,这两个解决方案之间有很大差异 - 即共享内存可以由多个内核并行读取 - 这意味着没有硬件瓶颈会使两个实现大致相同吗?
复制缓存不友好,因此会破坏多核上的可扩展性,因为它会加剧对作为主内存的共享资源的争用。
最终,Erlang风格的消息传递是为并发编程而设计的,而关于吞吐量性能的问题实际上是针对并行编程的。这是两个完全不同的主题,它们之间的重叠在实践中很小。具体而言,延迟通常与并发编程的上下文中的吞吐量同样重要,并且Erlang样式的消息传递是实现期望的延迟概况(即,始终如一的低延迟)的好方法。然后共享内存的问题不是读者和编写者之间的同步,而是低延迟内存管理。
答案 5 :(得分:3)
此处未提供的一个解决方案是主从复制。如果您拥有大型数据结构,则可以将更改复制到对其副本执行更新的所有从属服务器。
如果想要扩展到几台甚至无法在没有非常人为的设置的情况下共享内存的机器(从远程计算机的内存中读取/写入的块设备的mmap?),这一点特别有趣。
它的一个变体是有一个事务管理器,一个人很好地请求更新复制的数据结构,它将确保它同时提供一个并且只提供更新请求。这更像是用于mnesia table-data的主 - 主复制的mnesia模型,它被称为“大数据结构”。
答案 6 :(得分:3)
什么是大型数据结构?
一个人是另一个人小。
上周我与两个人交谈 - 一个人正在制作他使用过这个词的嵌入式设备 “大” - 我问他这意味着什么 - 他说超过256 KB - 后来在同一周a 那家伙在谈论媒体发行 - 他用“大”这个词我问他是什么 意思是 - 他想了一下并说“不适合一台机器”说20-100 TBytes
在Erlang中,“大”可能意味着“不适合RAM” - 所以使用4 GB的RAM <数据结构> 100 MBytes可能被认为是大型的 - 复制500 MB的数据结构 可能是个问题。复制小数据结构(比如说&lt; 10 MBytes)在Erlang中永远不会成为问题。
必须具有真正大型的数据结构(即不适合一台机器的数据结构) 复制并在多台机器上“镶边”。
所以我猜你有以下内容:
小数据结构没有问题 - 因为它们的数据处理时间很短 快速,复制速度快等等(仅仅因为它们很小)
大数据结构是一个问题 - 因为它们不适合一台机器 - 所以复制是必不可少的。
答案 7 :(得分:2)
目前的问题确实是锁定和缓存线一致性可能与复制更简单的数据结构(例如几百个字节)一样昂贵。
大多数情况下,一个巧妙编写的新多线程算法试图消除大部分锁定总是会更快 - 而且使用现代无锁数据结构会更快。特别是当您拥有设计良好的缓存系统,如Sun的Niagara芯片级多线程时。
如果您的系统/问题不容易分解为一些简单的数据访问,那么您就遇到了问题。并非所有问题都可以通过消息传递来解决。这就是为什么还有一些基于Itanium的超级计算机的销售,因为它们拥有TB的共享RAM和最多128个CPU在同一个共享内存上运行。它们比具有相同CPU功率的主流x86集群贵一个数量级,但您不需要分解数据。
到目前为止未提及的另一个原因是,当您使用多线程时,程序可以更容易编写和维护。消息传递和无共享方法使其更易于维护。
作为一个例子,Erlang从未被设计为使事情更快,而是使用大量线程来构建复杂的数据和事件流。
我想这是设计中的主要观点之一。在谷歌的网络世界中,你通常不关心性能 - 只要它可以在云中并行运行。通过消息传递,理想情况下可以在不更改源代码的情况下添加更多计算机。
答案 8 :(得分:1)
通常是消息传递语言(这在erlang中特别容易,因为它具有不可变的变量)优化了进程之间的实际数据复制(当然只有本地进程:你需要明智地考虑你的网络分布模式),所以这不是什么大问题。
答案 9 :(得分:0)
另一个并发范例是STM,即软件事务内存。 Clojure的裁判得到了很多关注。 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-Theses