【问题标题】:How are message-passing concurrent languages better than shared-memory concurrent languages in practice在实践中消息传递并发语言如何优于共享内存并发语言
【发布时间】:2011-12-19 15:39:09
【问题描述】:

我从事 Java 开发已经很多年了,但是在我开始进行 Android 开发之前,我从来没有过多地处理并发问题,并且突然开始发现“应用程序没有响应”和明显的死锁情况。

这让我意识到理解和调试其中一些并发问题是多么困难。 Scala 和 Go 等新语言如何提高并发性?它们如何更易于理解以及如何防止并发错误?有人可以提供展示优势的真实示例吗?

【问题讨论】:

    标签: scala concurrency go


    【解决方案1】:

    简化并发的三个主要竞争者是参与者、软件事务内存 (STM) 和自动并行化。 Scala 实现了所有这三个。

    演员

    Actor 在 Erlang 语言中找到了他们最显着的实现,据我所知,这就是这个想法的开始*。 Erlang 是围绕演员从头开始设计的。这个想法是演员本身就是彼此的黑匣子。它们仅通过传递消息进行交互。

    Scala 在其库中实现了actor,并且在外部库中提供了变体。在主库中,不强制执行黑盒性,但有易于使用的传递消息的方法,并且 Scala 可以轻松创建不可变消息(因此您不必担心发送带有一些内容的消息,然后在某个随机时间更改内容。

    actor 的优点是您不必担心复杂的共享状态,这确实简化了所涉及的推理。此外,您可以将问题分解为比线程更小的部分,并让参与者库找出如何将参与者捆绑到适当数量的线程中。

    缺点是,如果您尝试做一些复杂的事情,在您知道它成功之前,您需要处理很多逻辑来发送消息、处理错误等。

    软件事务内存

    STM 基于这样的思想,即最重要的并发操作是获取一些共享状态,对其进行修改,然后将其写回。所以它提供了一种方法来做到这一点;但是,如果它遇到一些问题——它通常会延迟检测到最后,此时它会检查以确保写入全部正确——它会回滚更改并返回失败(或重试)。

    这既是高性能(在只有适度争用的情况下,因为通常一切都很好)并且对大多数类型的锁定错误具有鲁棒性,因为 STM系统可以检测到问题(甚至可能会做一些事情,例如从较低优先级的请求中获取访问权并将其提供给较高优先级的请求)。

    与演员不同,尝试复杂的事情更容易,只要你能处理失败。但是,您还必须正确推理底层状态; STM 通过失败和重试来防止罕见的意外死锁,但如果你只是犯了一个逻辑错误并且某些步骤无法完成,STM 不允许它这样做。

    Scala 有一个 STM 库,它不是标准库的一部分,但正在考虑纳入。 Clojure 和 Haskell 都有完善的 STM 库。

    自动并行化

    自动并行化采取你不想考虑并发的观点;你只想让事情快速发生。因此,如果您有某种并行操作——例如,一次将一些复杂的操作应用于项目集合,并因此产生一些其他集合——您应该具有自动并行执行此操作的例程。 Scala 的集合可以以这种方式使用(有一个.par 方法可以将传统的串行集合转换为其并行模拟)。许多其他语言具有类似的功能(Clojure、Matlab 等)。


    编辑:实际上,Actor model 早在 1973 年就已被描述,可能是受 Simula 67 早期工作的启发(使用协程而不是并发); 1978 年出现了相关的Communicating Sequential Processes。因此,Erlang 的功能在当时并不是独一无二的,但该语言在部署 Actor 模型方面是独一无二的。

    【讨论】:

    • 在我看来,自动并行化意味着:将串行程序按原样传递给编译器/工具会产生并行化版本的程序。并行版本的运行速度必须至少与串行版本一样快。
    • @Atom - 这是最理想的,但几乎没有程序可以成功地做到这一点,因为了解并行方法是否正确和更快所需的分析是相当可观的。如果设置(主要通过导入)始终使用集合的并行版本,则可以在 Scala 中部分地做到这一点。也许我应该称之为“半自动”;我当然同意它不是全自动的(在 Scala 或几乎其他任何东西中)。
    • +1。但是,使用 .par。并不排除考虑并发性的需要。如果您正在修改可变状态,它不会以任何方式保护您。不过,它确实有助于不必创建线程等。
    • @Matthew Farwell - 同意;它大大减轻了负担(“这最好是可并行化的,而不是依赖于可变状态”),但并没有消除它。
    • @RexKerr 您忽略了 GCC 自动转换为 SIMD 程序的数百万个程序。
    【解决方案2】:

    对我来说,与传统并发模型相比,使用 Scala (Akka) Actor 有几个优势:

    1. 使用像演员这样的消息传递系统可以让您轻松处理共享状态。例如,我经常将一个可变数据结构包装在一个actor中,因此访问它的唯一方法是通过消息传递。由于参与者总是一次处理一条消息,这确保了对数据的所有操作都是线程安全的。
    2. Actor 部分消除了处理生成和维护线程的需要。大多数actor库将处理跨线程分发actor,因此您只需要担心启动和停止actor。通常,我会创建一系列相同的 Actor,每个物理 CPU 核心一个,并使用负载均衡器 Actor 将消息均匀地分配给它们。
    3. 参与者可以帮助提高系统可靠性。我使用 Akka actor,其中一个功能是您可以为 actor 创建一个主管,如果一个 actor 崩溃,主管将自动创建一个新实例。这可以帮助防止线程崩溃并且您被半运行程序卡住的情况。根据需要启动新演员并与在另一个应用程序中运行的远程演员一起工作也非常容易。

    您仍然需要对并发和多线程编程有充分的了解,因为仍然可能出现死锁和竞争条件,但是参与者可以更轻松地识别和解决这些问题。我不知道这些对 Android 应用有多大影响,但我主要从事服务器端编程,而使用 Actor 使开发变得更加容易。

    【讨论】:

      【解决方案3】:

      在惯用的 Go 程序中,线程通过通道传递状态和数据。 这可以在不需要锁的情况下完成(通道仍然使用引擎盖下的锁定)。通过通道将数据传递给接收者,意味着数据所有权的转移。一旦你通过通道发送了一个值,你就不应该再对它进行操作,因为现在接收它的人“拥有”它。

      但是,应该注意的是,Go 运行时不会以任何方式强制执行这种“所有权”转移。通过通道发送的对象不会被标记或标记或类似的东西。这只是一个约定。因此,如果您愿意,您可以通过改变您之前通过通道发送的值来打自己的脚。

      Go 的优势在于 Go 提供的语法(启动 goroutine 和通道的工作方式)使编写正确运行的代码变得容易得多,从而防止竞争条件和死锁。 Go 清晰的并发机制使您可以很容易地推断程序中将要发生的事情。

      附带说明:如果您真的想使用 Go 中的标准库,它们仍然提供传统的互斥锁和信号量。但您显然是自行决定并承担风险。

      【讨论】:

      • Go 的频道本质上和演员的邮箱是一样的,不是吗?
      • 据我了解,Go 的方法基于 Tony Hoare 的“CSP”(通信顺序过程)工作。一些信息可以在这里找到:usingcsp.com.
      • 嗯,主要区别在于通信是否同步;我想我会将它们归为同一类,即使两者背后的理论不同。实际上,在大多数情况下,您面临同样的问题;问题只是以不同的方式表现出来(邮箱已满与永远等待发送消息)。
      • Go 并发 bug 最多的原因是通道使用不正确。事情不是靠魔法发生的。并发仍然很难。 Go 通道并不能保护您免于编写错误的并发代码。
      【解决方案4】:

      Scala Actor 的工作原理是无共享的,因此没有锁(因此也没有死锁)! Actor 侦听消息,并由包含 Actor 可以处理的内容的代码调用。

      【讨论】:

      • 如果两个 Actor 正在等待来自对方的消息,而他们永远不会收到消息,则在基于 Actor 的系统中可能会发生死锁。然而,这比在环境(处理器速度、可用处理器……)可以改变时序等的共享内存系统中更容易推理。 dalnefre.com/wp/2010/08/dining-philosophers-in-humus
      • 这很好——感谢分享!尽管我猜想编写这样的代码会让你在 Scala 中陷入僵局会更加困难。我猜演员互相传递消息可能是一种有趣的编程方式。
      猜你喜欢
      • 2017-05-19
      • 1970-01-01
      • 2010-12-23
      • 1970-01-01
      • 2012-01-13
      • 1970-01-01
      • 1970-01-01
      • 2011-05-15
      • 2012-01-01
      相关资源
      最近更新 更多