【问题标题】:How do functional languages handle shared state data?函数式语言如何处理共享状态数据?
【发布时间】:2019-03-27 02:10:50
【问题描述】:

我一直在学习函数式编程,发现它确实可以使并行性更容易处理,但我不明白它如何使处理共享资源更容易。我见过人们谈论变量不变性是一个关键因素,但这如何帮助两个线程访问同一资源?假设两个线程正在向队列添加请求。他们都获得了队列的副本,添加了他们的请求来制作新副本(因为队列是不可变的),然后返回新队列。第一个返回请求将被第二个覆盖,因为每个线程获得的队列副本不存在其他线程的请求。所以我假设在功能语言中有一个锁定机制a la mutex?那么,这与解决问题的命令式方法有何不同呢?还是函数式编程的实际应用仍然需要一些命令式操作来处理共享资源?

【问题讨论】:

    标签: concurrency functional-programming mutex


    【解决方案1】:

    只要您的全球数据可以更新。你打破了纯功能范式。从这个意义上说,您需要某种命令式结构。然而,这很重要,大多数函数式语言都提供了一种方法来做到这一点,而且无论如何你都需要它能够与世界其他地方进行通信。 (最复杂的形式是 Haskell 的 IO monad。)除了一些其他同步库的简单绑定之外,如果可能的话,他们可能会尝试实现无锁、无等待的数据结构。

    一些方法包括:

    • 可以安全地访问只写入一次且永不更改的数据,无需锁定或在大多数 CPU 上等待。 (通常有一个内存栅栏指令来确保内存以正确的顺序更新生产者和消费者。)
    • 某些数据结构(例如差异列表)具有您可以在不使任何现有数据失效的情况下进行更新的属性。假设您有关联列表[(1,'a'), (2,'b'), (3,'c')],并且您想通过将第三个条目更改为'g' 来更新。如果您将此表达为(3,'g'):originalList,那么您可以使用新版本更新当前列表,并保持originalList 有效且不变。任何看到它的线程仍然可以安全地使用它。
    • 即使您必须绕过垃圾收集器,每个线程也可以创建自己的共享状态的线程本地副本,只要原始副本在复制时不会被删除。底层的低级实现将是一个生产者/消费者模型,它以原子方式更新指向状态数据的指针,并在更新和复制操作之前插入内存栅栏指令。
    • 如果程序有原子比较和交换的方法,并且垃圾收集器知道,则每个线程都可以使用接收-复制-更新模式。只要任何线程正在使用旧数据,线程感知垃圾收集器就会保留旧数据,并在最后一个线程完成时回收它。这不需要在软件中锁定(例如,在现代 ISA 上,递增或递减字大小的计数器是一种原子操作,原子比较和交换是无需等待的)。
    • 函数式语言可以添加一个扩展来调用以其他语言编写的 IPC 库,并就地更新数据。在 Haskell 中,这将使用 IO monad 定义以确保顺序内存一致性,但几乎每种函数式语言都有某种方式与系统库交换数据。

    因此,函数式语言确实提供了一些对高效并发编程有用的保证。例如,大多数当前的 ISA 在最多只有一个写入器时不会对多个读取器线程施加额外的开销,不会发生某些一致性错误,并且函数式语言非常适合表达这种模式。

    【讨论】:

      猜你喜欢
      • 2016-07-18
      • 1970-01-01
      • 2022-11-30
      • 1970-01-01
      • 1970-01-01
      • 2011-01-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多