Haskell 线程间的评估同步

Haskell evaluation synchronisation between threads

我试图了解 GHC Haskell 如何在线程之间同步计算“基本”值(即不是 IORef、TVar 等)。我搜索了有关此的信息,但没有找到任何明确的信息。

以下面的例子程序为例:

import Control.Concurrent

expensiveFunction x = sum [1..x] -- Just an example

val = expensiveFunction 12345

thread1 = print val

thread2 = print val

main = do
    forkOS thread1
    forkOS thread2

我知道值 va​​l 最初将由未计算的闭包表示。为了打印 val,程序必须首先评估它。一旦对顶层绑定进行了评估,就不需要再次对其进行评估。

  1. “val”的表示是否由单独的线程共享?

  2. 如果由于某种原因thread1先完成求值,是否可以通过换出指针将最终计算值传递给thread2?那将如何同步?

  3. 如果线程 1 在线程 2 需要值时正忙于求值,线程 2 是等待它完成还是他们都先求值?

在 GHC 编译的程序中,值会经历三个(大概)求值阶段:

  1. 砰的一声。这是他们开始的地方。
  2. 黑洞。强制时,thunk 将转换为黑洞并开始计算。请求黑洞值的其他线程将改为将自己添加到黑洞更新时的通知列表中。 (另外,如果thunk本身尝试访问黑洞,它会短路异常,而不是永远等待。)
  3. 已评价。计算完成后,它的最后一项任务是将黑洞更新为普通值(无论如何,WHNF 值)。

在这些阶段转换期间更新的指针与其他线程共享并且 不受竞争条件的影响。这意味着,在极少数情况下,两个(或更多)线程都可能在阶段 1 中看到一个指针并且都执行 1 -> 2 转换;在这种情况下,两者都会评估 thunk,并且转换 2 -> 3 也会发生两次。不过,值得注意的是,1 -> 2 转换通常比它正在替换的计算快得多(本质上只是一两次内存访问),部分原因是竞争很难触发。

因为语言纯正,所以跑马圈地会得出同一个答案。所以这里没有语义上的困难。但在极少数情况下,可能会重复一些工作。每次 1 -> 2 转换的锁开销都比这种轻微的重复更好,这是非常非常罕见的。 (如果你发现它在你的情况下,考虑手动保护正在共享的任何昂贵的东西的评估!)

推论:必须非常小心处理不安全的 IO a -> a 函数族;有些保证对结果 a 的评估同步,有些则不保证。如果您的 IO a 操作并不像您承诺的那样纯粹,并且竞争导致它被执行两次,则可能会出现各种奇怪的 heisenbug。