8

偶然阅读有关线程安全的ZeroMQ 常见问题解答。

我的多线程程序在 ZeroMQ 库中的奇怪地方不断崩溃。我究竟做错了什么?

ZeroMQ 套接字不是线程安全的。《指南》中对此进行了详细介绍。

简短的版本是套接字不应在线程之间共享。我们建议为每个线程创建一个专用套接字。

对于每个线程的专用套接字不可行的情况,当且仅当每个线程在访问套接字之前执行完整的内存屏障时,才可以共享套接字。大多数语言都支持互斥锁或自旋锁,它们将代表您执行完整的内存屏障。

我的多线程程序在 ZeroMQ 库中的奇怪地方不断崩溃。
我究竟做错了什么?

以下是我的以下代码:

Celluloid::ZMQ.init
module Scp
    module DataStore
    class DataSocket
        include Celluloid::ZMQ 
            def pull_socket(socket)
                @read_socket = Socket::Pull.new.tap do |read_socket|
                    ## IPC socket
                    read_socket.connect(socket)
                end
            end

            def push_socket(socket)
                @write_socket = Socket::Push.new.tap do |write_socket|
                    ## IPC socket
                    write_socket.connect(socket)
                end
            end

            def run
                pull_socket and push_socket and loopify!
            end

            def loopify!
                loop {
                   async.evaluate_response(read_socket.read_multipart)
                }
            end

            def evaluate_response(data)
                return_response(message_id,routing,Parser.parser(data))
            end

            def return_response(message_id,routing,object)
                data = object.to_response
                write_socket.send([message_id,routing,data])
            end
        end
    end
end  

DataSocket.new.run 

现在,有几件事我不清楚:

1) 假设async产生一个新的Thread(每次)并且write_socket在所有线程之间共享并且ZeroMQ表示它们的套接字不是线程安全的。我当然看到了write_socket线程安全问题。
(顺便说一句,到目前为止,在所有端到端测试中都没有遇到这个问题。)

问题1:我对此的理解是否正确?

为了解决这个问题,ZeroMQ 要求我们使用 Mutex、Semaphore 来实现这一点。

这导致问题2

2) 上下文切换。

给定线程应用程序可以随时进行上下文切换。查看 ffi-rzmq 代码Celluloid::ZMQ .send()内部调用send_strings(),内部调用send_multiple()

问题 2:上下文切换可以发生在(任何地方)内部(甚至在关键部分)(这里)[ https://github.com/chuckremes/ffi-rzmq/blob/master/lib/ffi-rzmq/socket.rb#L510 ]

这也可能导致数据排序问题。

我的以下观察是否正确?

笔记:

Operating system ( MacOS, Linux and CentOS )  
Ruby - MRI 2.2.2/2.3.0
4

2 回答 2

7

没有人应该将应用程序的健壮性置于如履薄冰的风险

请原谅这个故事读得相当长,但作者终生的经验表明,原因远比任何少数 SLOC(可能令人怀疑或看起来神秘或根本原因无知)试图通过实验找到如何

初始说明

虽然 ZeroMQ 几十年来一直被推广为零共享(零阻塞、(几乎)零延迟和其他一些设计准则。阅读有关利弊的最佳地点是 Pieter HINTJENS 的书籍,而不仅仅是神话般的“代码连接,第 1 卷”,但也是真正的社会领域的高级设计和工程)哲学,最近的 API 文档已经引入并宣传了一些恕我直言的特性,这些特性与分布式计算的这些基石原则有着轻松的关系,那不要对零共享那么尖锐的哨声那么响亮。这就是说,我仍然是一个零分享的人,所以请从这个角度来看这篇文章的其余部分。

回答1:
不,先生。——或者更好——是的,不是的,先生。

ZeroMQ 不要求使用 Mutex/Semaphore 屏障。这与 ZeroMQ 设计准则相矛盾。

是的,最近的 API 更改开始提到(在一些额外的条件下)人们可能会开始使用共享套接字......带有(许多)额外的措施......所以暗示被颠倒了。如果一个人“想要”,他也会采取所有额外的步骤和措施(并支付所有最初隐藏的设计和实施成本,以“允许”共享玩具(希望)在与其他人的主要(不必要)战斗中幸存下来无法控制的分布式系统环境——因此突然也承担了失败的风险(由于许多明智的原因,在最初的 ZeroMQ 零共享传播中并非如此)——所以,用户决定走哪条路。这是公平的. ).

健全和健壮的设计恕我直言,仍然最好按照最初的 ZeroMQ API 和宣传进行开发,其中零共享是一个原则。

答案 2:
ZeroMQ 数据流排序始终存在设计上的主要不确定性,ZeroMQ 设计准则之一使设计人员不要依赖于消息排序和许多其他不受支持的假设(例外情况适用)。可以肯定的是,任何分派到 ZeroMQ 基础设施的消息要么作为完整消息传递,要么根本不传递。因此,可以肯定的是,交付时不会出现碎片残骸。有关更多详细信息,请阅读下文。


ThreadId不能证明任何事情(除非inproc使用了传输类)

鉴于 ZeroMQ 数据泵引擎的内部设计,a 的实例化
zmq.Context( number_of_IO_threads )决定了产生多少线程来处理未来的数据流。这可能是任何地方 { 0, 1: default, 2, .. } 几乎耗尽内核固定的最大线程数。值 0 给出了一个不浪费资源的合理选择,以防inproc://传输类实际上是数据流的直接内存区域映射处理(实际上永远不会流 ang 直接固定到接收端的着陆区) socket-abstraction :o) ) 并且这样的工作不需要线程。
除此之外,<aSocket>.setsockopt( zmq.AFFINITY, <anIoThreadEnumID#> )允许微调与数据相关的 IO-“液压”,以便将线程负载优先级、负载平衡、性能调整到枚举池中zmq.Context()-instance 的 IO 线程,并从上面列出的设计和数据流操作方面的更好和最佳设置中获益。


基石元素是Context()s' 实例,
而不是 aSocket()的实例

一旦一个Context()实例被实例化和配置(参考上面为什么和如何),它(几乎)是免费共享的(如果设计无法抗拒共享或需要避免设置一个成熟的分布式-计算基础设施)。

换句话说,大脑总是在zmq.Context()'s 实例内部——所有与套接字相关的 dFSA 引擎都在那里设置/配置/操作(是的,即使语法是<aSocket>.setsockopt(...)这样的效果在大脑内部实现——在各自zmq.Context的-不在某些从A到B的电线中。

最好不要分享<aSocket> (即使 API-4.2.2+ 承诺你可以)

到目前为止,人们可能已经看到了很多代码片段,其中 ZeroMQ 上下文和它的套接字被实例化并立即处理掉,只连续提供几个 SLOC-s,但是——这并不意味着,这样的实践是明智的或因任何其他需要而调整的,而不是一个非常学术的例子(由于图书出版商的政策,这只是需要以尽可能少的 SLOC 印刷)。

即使在这种情况下,也应该存在关于基础设施设置/拆除的巨大成本的公平警告zmq.Context,因此为避免任何概括,此类代码的任何复制/粘贴副本越少,仅用于此类说明性的人手不足目的。

想象一下任何单个实例需要进行的实际设置Context——准备一个各自的 dFSA 引擎池,维护它们各自的配置设置以及所有与传输类特定硬件相关的套接字端点池 + 外部 O /S-services 处理程序、循环事件扫描器、缓冲内存池分配 + 它们的动态分配器等,等等。这一切都需要时间和 O/S 资源,所以要明智和小心地处理这些(自然)成本对于调整后的间接费用,如果性能不受影响。

如果仍然不确定为什么要提及这一点,想象一下是否有人坚持在发送数据包后立即拆除所有 LAN 电缆,并且需要等到安装新电缆后才需要发送下一个数据包出现。希望现在可以更好地理解这种“合理的实例化”观点,并提出分享(如果有的话)一个zmq.Context()-instance(s)的论据,而无需为尝试共享 ZeroMQ 套接字实例而进行任何进一步的斗争(即使新成为(几乎)线程安全本身)。

如果将 ZeroMQ 理念视为高性能分布式计算基础设施的高级设计宣传,那么它是稳健的。只调整一个(次要)方面通常不会调整所有的努力和成本,因为在如何设计安全和高性能系统的全球观点上,结果不会更好(甚至是绝对可共享的风险-免费的(如果可能的话)套接字实例不会改变这一点,而声音设计、简洁代码和合理可实现的测试能力和调试的所有好处都将丢失)如果只是这一个细节被改变——所以,而是将另一根线从现有大脑拉到这样一个新线程,或者为一个新线程配备它自己的大脑,它将在本地处理它'

如果仍有疑问,试着想象一下如果你的国家奥林匹克曲棍球队在比赛期间只共用一根曲棍球棒,会发生什么。或者,如果您家乡的所有邻居共享同一个电话号码来接听所有来电(是的,所有电话和手机同时响铃,共享同一个号码),您会怎么想。这会有多好?


语言绑定不需要反映所有可用的 API 功能

在这里,人们可以提出,并且在某些情况下是正确的,并非所有 ZeroMQ 语言绑定或所有流行的框架包装器都将所有 API 细节暴露给用户以进行应用程序级编程(这篇文章的作者已经挣扎了很长时间有了这样的遗留冲突,这仍然是无法解决的权利,因此不得不费尽心思才能找到任何可行的方法来解决这个事实——所以它(几乎)总是可行的)


结语:

公平地指出,ZeroMQ API 4.2.2+ 的最新版本开始蔓延最初的福音化原则。

尽管如此,值得记住的焦虑纪念品

(加了重点,不大写)

线程安全

ØMQ既有线程安全的套接字类型,也有非线程安全的套接字类型。应用程序不得使用来自多个线程的非线程安全套接字,除非将套接字从一个线程迁移到另一个具有“全栅栏”内存屏障的线程。

以下是线程安全套接字: * ZMQ_CLIENT* ZMQ_SERVER* ZMQ_DISH* ZMQ_RADIO* ZMQ_SCATTER*ZMQ_GATHER

虽然在某些人看来这篇文章很有希望,但在设计先进的分布式计算系统时,调用服务障碍是最糟糕的事情,其中​​性能是必须的。

人们希望看到的最后一件事是阻止自己的代码,因为这样的代理进入了一种基本上无法控制的阻塞状态,没有人可以从那里(无论是内部的代理本身,也不是来自外部的任何人),以防远程代理永远不会传递预期的事件(在分布式系统中,这可能由于多种原因或在很多不受控制的情况下发生)。

构建一个容易挂起的系统(带着支持(但天真地使用)语法可能性的宽大笑容)确实没有什么可高兴的事情,更不严肃的设计工作。

在这里也不会感到惊讶,许多额外的(最初不可见)限制适用于使用 shared-{ hockey-stick | 的新动作。电话 } API:

ZMQ_CLIENT套接字线程安全的。他们不接受ZMQ_SNDMORE发送而不是 接收的选项ZMQ_RCVMORE这将它们限制为单部分数据。目的是扩展 API 以允许分散/收集多部分数据。

c/a

Celluloid::ZMQ在其关于支持的套接字类型的部分中没有报告任何这些新的 API(共享几乎是宽容的罪过)套接字类型,因此没有预期的好消息先验和Celluloid::ZMQ主活动似乎在 2015 年的某个地方消失了,所以从这个角度来看,期望应该有点现实。

这就是说,在通知背后可能会发现一个有趣的点:

在你用 构建你自己的分布式赛璐珞系统之前Celluloid::ZMQ,一定要看看DCell并确定它是否适合你的目的。


最后但同样重要的是,在另一个事件循环中组合事件循环系统是一项痛苦的工作。试图将嵌入式硬实时系统集成到另一个硬实时系统中,甚至可以在数学上证明自己是不可能的。

类似地,使用另一个基于代理的组件构建多代理系统会带来额外种类的冲突和竞争条件,如果满足相同的资源,它们会被两者利用(无论是有意还是通过“只是”一些功能性副作用)(多个)基于代理的框架。

不可挽救的相互死锁只是这些冲突中的一种,它在无意识的设计尝试中引入了最初看不到的麻烦。单代理系统设计之外的第一步使一个人失去更多的保证,这些保证在多代理(分布式)之前没有被注意到,因此开放的思想并准备好学习许多“新”概念和专注在许多新的问题上需要仔细观察和努力避免是一个非常重要的先决条件,以免(不知不觉地)引入模式,这些模式现在实际上是分布式系统(多代理)领域中的反模式。

至少
你已经被警告了
:o)

于 2017-08-06T16:16:30.600 回答
0

这个答案不是解决您问题的好方法,并且绝对符合 user3666197 的建议。我认为这个解决方案有潜力发挥作用,但在大范围内,由于互斥体拥塞可能会产生性能成本。

问题 1:假设异步生成新线程(每次)并且 write_socket 在所有线程之间共享,而 zeromq 表示它们的套接字不是线程安全的。我当然看到 write_socket 遇到线程安全问题。(顺便说一句,到目前为止,在所有端到端测试中都没有遇到这个问题。)我对此的理解是否正确?

根据我对文档的理解,是的,这可能是一个问题,因为套接字不是线程安全的。即使您没有遇到此问题,它也可能会在稍后弹出。

问题 2:上下文切换可以在内部(任何地方)发生(甚至在关键部分)

是的,所以我们可能解决这个问题的一种方法是使用互斥体/信号量来确保我们不会在错误的时间发生上下文切换。

我会做这样的事情,但根据被调用的方法不是线程安全的,可能会有更好的方法:

Celluloid::ZMQ.init
module Scp
  module DataStore
    class DataSocket
      include Celluloid::ZMQ

      def initialize
        @mutex = Mutex.new
      end

      def pull_socket(socket)
        Thread.new do
          @mutex.synchronize do
            @read_socket = Socket::Pull.new.tap do |read_socket|
              ## IPC socket
              read_socket.connect(socket)
            end
          end
        end.join
      end

      def push_socket(socket)
        Thread.new do
          @mutex.synchronize do
            @write_socket = Socket::Push.new.tap do |write_socket|
              ## IPC socket
              write_socket.connect(socket)
            end
          end
        end.join
      end

      def run
        # Missing socket arguments here
        pull_socket and push_socket and loopify!
      end

      def loopify!
        Thread.new do
          @mutex.synchronize do
            loop {
              async.evaluate_response(read_socket.read_multipart)
            }
          end
        end.join
      end

      def evaluate_response(data)
        return_response(message_id,routing,Parser.parser(data))
      end

      def return_response(message_id,routing,object)
        data = object.to_response
        write_socket.send([message_id,routing,data])
      end
    end
  end
end

DataSocket.new.run
于 2017-08-06T13:49:12.453 回答