函数式语言如何处理共享状态数据?

How do functional languages handle shared state data?

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

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

一些方法包括:

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

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