线程之间的 Haskell 评估同步

jfo*_*erg 6 haskell ghc

我试图了解 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
Run Code Online (Sandbox Code Playgroud)

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

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

  2. 如果由于某种原因,线程 1 首先完成评估,是否可以通过换出指针将最终计算出的值传送给线程 2?这将如何同步?

  3. 如果线程 1 正忙于评估线程 2 想要该值时,线程 2 是等待它完成还是它们都争先恐后地评估它?

Dan*_*ner 6

在 GHC 编译的程序中,值经历三个(-ish)评估阶段:

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

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

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

推论:必须非常小心地处理不安全IO a -> a的函数系列;有些保证对结果的评估同步,a有些则不保证。如果你的IO a行为不像你承诺的那么纯粹,并且一场比赛导致它被执行两次,那么各种奇怪的黑森虫都会发生。