共享内存与消息传递如何处理大型数据结构?

wso*_*son 57 memory parallel-processing concurrency erlang go

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

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

但是,请考虑许多客户端希望对内存中的单个大型数据结构进行并行只读访问的情况 - 如后缀数组.

我的问题:

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

  • 如何在消息传递上下文中处理此问题?是否存在可以访问数据结构的单个进程,客户端只需要从中顺序请求数据?或者,如果可能的话,数据是否会被分块以创建几个保存块的进程?

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

rvi*_*ing 28

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

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


Jav*_*ier 27

  • 是的,在这种情况下,共享状态可能更快.但只有你可以放弃锁,这只有在绝对只读时才可行.如果它"大多是只读"那么你需要一个锁(除非你设法写无锁结构,警告他们甚至比锁更棘手),然后你很难让它像快速作为一种良好的消息传递架构.

  • 是的,您可以编写一个"服务器进程"来共享它.使用非常轻量级的流程,与编写小型API来访问数据相比,它并不重要.想象一个'拥有'数据的对象(在OOP意义上).以块为单位拆分数据以增强并行性(在DB圈中称为"分片")有助于处理大型情况(或者如果数据存储在慢速存储中).

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

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

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

通过通信共享内存,不通过共享内存进行通信.

这个想法是:当你在线程之间共享一块内存时,避免并发访问的典型方法是使用锁来进行仲裁.Go样式是使用引用传递消息,线程仅在接收消息时访问内存.它依赖于一些程序员纪律; 但是会产生非常干净的代码,可以很容易地进行校对,因此调试相对容易.

优点是您不必在每条消息上复制大块数据,也不必像某些锁实现那样有效地清除缓存.如果风格能够带来更高性能的设计,那还为时尚早.(特别是因为当前的Go运行时在线程调度上有些天真)

  • @Jon Harrop:不,不幸的是,它本身并不是原子的.在多核系统中,可变引用可以由不同的内核缓存,因此如果您不使用某种机制(如内存屏障)来确保访问的某种部分顺序,则最终会出现不一致的行为.仅仅编写指针是不够的.您需要避免锁定是一种真正无锁的算法.基本技巧是有效的原子指针替换; 但它必须做得正确,而不是依赖"它是一个单一的指令,所以它是原子的"神话 (2认同)

Nic*_*son 12

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

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


Gre*_*ers 11

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

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

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


ja.*_*ja. 5

什么是数据结构?

一个人大,另一个人小。

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

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

真正大的数据结构(即那些不能放在一台机器上的数据结构)必须在多台机器上复制和“条带化”。

所以我想你有以下几点:

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

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


Jon*_*rop 5

请注意,您的问题从技术上讲是荒谬的,因为消息传递可以使用共享状态,所以我将假定您的意思是消息传递采用深度复制来避免共享状态(如Erlang当前所做的那样)。

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

使用共享的状态将是一个很大更快。

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

两种方法都可以使用。

考虑到现代CPU和内存的体系结构,两种解决方案之间有很大的不同-即共享内存可以由多个内核并行读取吗-意味着没有硬件瓶颈会导致这两种实现大致执行相同的工作吗?

复制是不友好的缓存,因此会破坏多核的可伸缩性,因为它会加剧对作为主内存的共享资源的争用。

最终,Erlang样式的消息传递是为并发编程而设计的,而有关吞吐量性能的问题实际上是针对并行编程的。这是两个截然不同的主题,实际上它们之间的重叠很小。具体来说,在并发编程的情况下,延迟通常与吞吐量一样重要,而Erlang样式的消息传递是实现所需延迟配置文件(即始终较低的延迟)的好方法。共享内存的问题不是读写器之间的同步,而是低延迟的内存管理。