【问题标题】:Are there concurrent designs where the actor model isn't good for?是否存在actor模型不适合的并发设计?
【发布时间】:2013-06-21 10:12:19
【问题描述】:

我注意到我遇到的所有设计都可以使用 actor 模式进行多线程 - 将每个工作模块分成不同的 actor 并使用消息队列(对我来说是 .NET ConcurrentQueue)来传递消息。还有哪些好的多线程模型?

【问题讨论】:

  • 我认为你的问题没有引起人们的注意可能是因为没有多少人被教导过这种事情。人们在信号量、共享内存等的上下文中学习线程,通常就是这样。像微软这样的公司正在尽最大努力向程序员隐藏线程——线程池中的任务、带有完成事件的异步 IO 等等。CSP 本身就是一个进程演算,如果这个短语不适合普通程序员的话,我不会'不知道会怎样!这种糟糕的教学的结果是,相当多的程序员发现线程很难而且有点可怕。

标签: multithreading design-patterns concurrency


【解决方案1】:

我认为,通信顺序进程是一种比参与者模型更好的并发模型。它解决了 Actor 模型(和其他模型)的许多问题,例如死锁、活锁、饥饿。看看this,更实用的是this。

主要区别如下。在 Actor 模型中,消息是异步发送的。但是在 CSP 中消息是同步发送的;在接收者准备好接收之前,发送者无法发送。

这一简单的限制让世界变得与众不同。如果你有一个不正确的设计,可能会出现死锁,那么在actor模型中它可能会发生也可能不会发生(它通常只在向老板演示时才会发生......)。然而,在 CSP 中,死锁总是会发生,让您毫无疑问地认为您的设计是不正确的。好的,所以你仍然需要修复它,但没关系;解决你知道的问题比尝试详尽地测试是否存在问题(你在actor模型中的唯一选择)容易得多。

CSP 的严格同步方法似乎会导致响应时间问题;例如,人们担心 GUI 线程无法继续前进,因为它无法向繁忙的工作线程发送消息,而该工作线程还没有达到“读取”的程度。您要做的是确保工作负载分布在足够多的线程上,以便它们都可以在可接受的时间内重新等待新消息。 CSP 不会让你侥幸逃脱。演员模型确实如此,但不要被欺骗;你只是在制造未来的问题。

在 .NET 中,ConcurrentQueue 不是 CSP 的正确原语,除非您在顶部添加一个同步机制。我也在 TCP 套接字之上添加了严格的同步。事实上,我通常最终会编写某种库来抽象套接字和管道,因此对于“进程”(正如他们在 CSP 用语中所说的那样)是这台机器上的线程还是整个其他进程变得无关紧要在网络连接结束的另一台机器上。不错 - 从一开始就内置了可扩展性。

我已经用 CSP 方式做了 23 年了,我不会再用其他方式了。以这种方式构建了一些具有数千个线程的大型系统。

==编辑==

似乎这个答案仍然引起了一些关注,所以我想我会添加它。对于 Windows 开发人员,任务并行库有 DataFlow 命名空间。它必须单独下载。微软如此描述它:“这种数据流模型通过为粗粒度数据流和流水线任务提供进程内消息传递来促进基于参与者的编程。”优秀!它使用像BufferBlocks 这样的类作为通信渠道。重要的是 BufferBlock 有一个默认为 Unbounded 的 BoundedCapacity 属性,它适合 Actor 模型。将此值设置为 1,您现在已将其转换为 CSP 样式的通信通道。

【讨论】:

  • 有趣——使用同步消息发送进行测试/调试是否有任何好处(检测任何死锁),然后禁用发布同步(例如,获得更好的性能,因为发送线程不必等待?)。另外,您在第四段中指的是哪种“未来问题”?内存耗尽?
  • @JeremyFriesner - 查看单独的答案
  • @bazza 嗨,I'm looking for a way to use CSP in F#。如何使用 DataFlow?
  • @mamcx 恐怕我对 F# 一无所知。大概你可以只使用数据流?另请注意,虽然您可以通过将有界容量设置为 1 来接近 CSP,但要使其成为真正的 CSP,它必须为 0。
【解决方案2】:

最后补充一下,除了 CSP 之外,还有各种其他多线程模型。这个Wikipedia page 列出了其他几个,例如CCS、ACP 和LOTOS。阅读这些文章,您会发现有一个深邃而黑暗的洞穴,学者们在其中游荡,等待着扑向一个流浪的软件开发人员。

问题在于,学术上的默默无闻通常意味着完全缺乏实用、可用级别的工具和库。将一个健全的、经过验证的学术研究转化为一组库和工具需要付出很多努力。对于更广泛的软件社区来说,几乎没有真正的动力去研究一篇理论论文并将其变为现实。

我喜欢 CSP,因为基于 select() 或 pselect() 实现您自己的 CSP 库实际上非常简单。我已经这样做了好几次了(我必须学习代码重用),再加上肯特大学的好人为喜欢 Java 的人编写了 JCSP。我不建议在 Occam 中开发(尽管它仍然几乎是可能的);支持和可维护性将成为未来的问题。 CSP 可能是最容易进入的一种,鉴于其良好的特性,它非常值得。

【讨论】:

  • 现在即将推出的是一个新的 Clojure core.async 库,它基于 CSP 和 Occam 的想法,但具有不与实际线程绑定的优点(优于 JCSP)。
  • 异步部分偏离了 CSP 的意图(严格同步),听起来 Clojure 将类似于 Actor 模型。这当然没有错! Actor 模型和 CSP 系统的总体形状和架构几乎相同。然而,CSP 变体在数学上是可分析的(因为同步性),而 Actor Model 变体则不是。没有被绑定到真正的线程?嗯,Ada 运行时曾经这样做,我记得“光荣的一天”(所以我听说)Greenhill 的 VxWorks 的 Ada 编译器开始使用真正的线程!
  • @bazza 您可以编辑您知道的答案,而不是留下 3 个答案! ;-) 所有有趣的东西,谢谢。
  • @DavidRoussel,他们越来越长了,不是吗!很高兴这很有趣,很高兴为您服务:)
【解决方案3】:

@JeremyFriesner

未来的问题

为了扩展我所说的“未来问题”的含义,我指的是在异步系统中,消息的发送者不知道接收者是否真的跟上了需求。发件人不知道,因为它只知道某个消息缓冲区已接受该消息。然后,当接收者愿意接受时,下面的传输(例如 tcp)继续将消息推送过来。

因此,在压力下,系统可能无法按要求执行,因为消息传输将不可避免地具有有限的能力来吸收接收者尚不能接受的消息。发件人只有在问题已经开始发展之后才发现这一点,到那时,采取任何措施可能为时已晚。

测试当然可以揭示这个问题,但你必须小心,测试确实已经耗尽了传输器吸收消息的能力。只是全速快速爆炸可能具有欺骗性。

当然,同步系统会带来额外的开销(“你准备好了吗?”、“不,还没有”、“现在?”、“是!”、“那么你来了”)发生在异步系统中。因此,平均而言,异步系统会更高效,实际上可能具有更高的吞吐量等。这就是世界上大多数系统实际上是异步的原因,也是系统并不总是达到原始系统的全部容量的原因网络带宽/处理时间可能会建议。在我看来,当接近满负荷时,异步系统往往不会优雅地限制。令牌总线(nb not 令牌环)是同步网络的一个很好的例子,具有完全可靠和确定的吞吐量,但比以太网和令牌环慢一点......

在我的问题中总是有足够的带宽,我选择同步路由是出于成功确定性的原因;我并没有在带宽上损失太多,但我损失了很多风险,这很好。

从同步转换为异步

也许,但它可能没有什么价值。在同步系统中,只有成功地平衡了线程之间的分工,它才能按要求工作。也就是说,有足够多的线程在执行慢速位,因此不会阻碍快速位。弄错了,系统肯定不够快。

但是这样做之后,您就拥有了一个系统,其中每个组件都能够无延迟地继续发送消息,因为它发送到的所有内容都已准备好并正在等待(因为您在平衡工作负载方面的技能和判断力)。因此,如果您确实转换为异步消息传输,那么您所做的就是在这些消息的传输中节省少量时间。您所做的更改不会导致工作负载得到更快的处理。但是,如果节省带宽是目标,那么它也许是值得的。

当然,进行这种平衡可能是一件困难的事情,而且处理 HDD 访问时间、网络等变量可能很难克服。我经常不得不实施“下一个可用”工作负载共享方案。但可以肯定的是,在像我和你一起玩的那些实时信号处理系统中,基本上是在处理像 OpenVPX 的 RapidIO 这样非常可靠的传输,你只是对数据进行求和(不处理数据库、磁盘等),并且数据速率非常高(如今 1GByte/sec 是完全可行的,事实上,我在 13 年前就处理过如此高的数据速率;那是我的工作)。严格同步意味着您要么绝对跟上数据速率,要么绝对不跟上。使用异步,它更像是一个...

适合所有人的实时操作系统!

拥有一个实时操作系统也是一个必不可少的组件,而现在它似乎是用于 Linux 的 PREEMPT_RT 补丁集,它为许多业内人士完成了这项工作。 Redhat 对它进行了预打包(RedHat MRG),但是对于来自 CERN 好心人的免费赠品 Scientific Linux 是好的和免费的!我强烈怀疑,如果使用 PREEMPT_RT,许多系统在接近其容量限制的情况下会更顺畅地工作 - 它可以很好地解决问题。

【讨论】:

    【解决方案4】:

    并发是一个引人入胜的话题,有很多实现方法,基本问题是 - “我如何协调并行计算?”。

    一些并发模型是:

    期货

    Futures 也称为 Promises 或 Tasks 是充当异步计算结果的代理的对象。当计算实际需要该值时,线程会冻结,直到计算完成,从而实现同步。

    Future 是 .NET 和 ES6 的首选并发模型。

    软件事务内存

    软件事务内存 (STM) 通过将操作分组到事务中来同步对共享内存的访问(很像锁)。任何单个事务只能看到共享内存的单个视图并且是原子的。这在概念上类似于有多少数据库处理并发。

    STM 是 Clojure 和 Haskell 的首选并发模型。

    演员模型

    Actor 模型专注于消息传递。 Actor 接收到消息并可以决定发送消息作为响应,生成其他 Actor,进行本地更改等。这可能是这些模型中最不紧密耦合的模型,因为 Actor 仅交换消息而没有其他内容。

    Actor 模型是 Erlang 和 Rust 的首选并发模型。


    请注意,与上面提到的语言不同,大多数语言没有 cannon 或首选的并发模型,即使是那些对一种模型表现出强烈偏好的语言通常也会将其他模型实现为库。

    我个人的观点是,Futures 在使用和推理的简单性方面优于 STM 和 Actors,但这些模型都没有本质上是“错误的”,而且我认为两者都没有缺点。您可以使用您喜欢的任何一个,而不会产生任何后果。

    【讨论】:

    【解决方案5】:

    最通用的并行处理模型是Petri Nets。它将计算表示为纯数据依赖图,表示最大并行度。所有其他模型都源于它。

    数据流计算模型http://www.cs.colostate.edu/cameron/dataflow.html、http://en.wikipedia.org/wiki/Dataflow_programming 几乎同样强大。它将 Petri 网的位置限制为只有一个输出弧。在实践中,这很有用,因为具有多个输出弧的地方很难实现,会导致不确定性,并且很少需要。

    Actor 模型是一种数据流模型,其中节点可能只有 2 条输入边 - 一条用于输入消息,一条用于 Actor 的状态。如果您要编写具有副作用和多个参数的函数,这是一个严重的限制。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-13
      • 1970-01-01
      • 2013-09-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多