10

宣传的 FP 功能之一是程序“默认并行”,并且自然适合现代多核处理器。事实上,减少一棵树本质上是平行的。但是,我不明白它如何映射到多线程。考虑以下片段(伪代码):

let y = read-and-parse-a-number-from-console
let x = get-integer-from-web-service-call
let r = 5 * x - y * 4
write-r-to-file

翻译器如何确定应该在线程上运行哪些树枝?在您获得x或在单独的线程上减少或表达式之后y会很愚蠢(即使我们从线程池中获取它),不是吗?那么不同的函数式语言是如何处理这个问题的呢?5 * xy * 4

4

2 回答 2

12

我们还没有完全做到。

纯声明式的程序(函数式风格包括在这个类别中,但其他一些风格也是如此)往往更适合并行化,因为所有数据依赖关系都是显式的。这使得程序员可以很容易地手动使用语言提供的原语来指定两个独立的计算应该并行完成,无论它们是否共享对任何数据的访问;如果一切都是不可变的并且没有副作用,那么改变事情完成的顺序不会影响结果。

如果语言强制执行纯度(如 Haskell、Mercury 等,但与鼓励但不强制执行纯度的 Scala、F# 等不同),那么编译器可能会尝试自动并行化程序,但不存在我知道的语言默认情况下会这样做。如果该语言允许未经检查的不纯操作,那么编译器通常不可能进行必要的分析来证明给定的自动并行化程序的尝试是有效的。所以我不希望任何这样的语言能够非常有效地支持自动并行化。

请注意,您编写的伪程序可能不是纯声明性代码。let y = read-and-parse-a-number-from-console并且let x = get-integer-from-web-service-call正在计算x并且y来自不纯的外部操作,并且程序中没有任何东西可以确定它们应该运行的顺序。一般来说,以任一顺序执行两个不纯操作都会产生不同的结果,并且在不同的线程中运行这两个操作会放弃对它们运行顺序的控制。因此,如果像这样的语言要自动并行化您的程序,它几乎肯定会引入可怕的并发错误,或者拒绝显着并行化任何东西。

然而,函数式风格仍然可以很容易地手动并行化这些程序。人类程序员可以说,您从控制台和网络读取的顺序几乎肯定无关紧要。知道没有共享的可变状态可以决定并行运行这两个操作而无需深入研究它们的实现(您必须在命令式算法中这样做,其中可能存在可变共享状态,即使它看起来不像来自界面)。

但是,强制纯语言的自动并行编译器的最大复杂性是知道要进行多少并行化。当您尝试在少量处理器上运行大量非常短暂的线程时,并行运行所有可能的计算极大地压倒了产生新线程的所有启动成本(更不用说上下文切换)的任何可能收益。编译器需要识别数量相当少的相当大的计算“块”,并在按顺序运行每个块的子计算的同时并行运行这些块。

但是只有“令人尴尬的并行”程序才能很好地分解成非常大的完全独立的计算。大多数程序更加相互依赖。因此,除非您只想能够自动并行化非常容易手动并行化的程序,否则您的自动并行化可能需要能够识别并并行运行部分相互依赖的“块”,让它们等待当他们到达真正需要一个应该由另一个“块”计算的结果的点时。这会在线程之间引入额外的同步开销,因此选择并行运行的逻辑需要更好,以击败仅按顺序运行所有内容的琐碎策略。

Mercury(一种纯逻辑编程语言)的开发人员正在研究解决这些问题的各种方法,从静态分析到使用分析数据。如果你有兴趣,他们的研究论文有更多的信息。我想其他研究正在用其他语言研究这个领域,但我对任何其他项目知之甚少。

于 2012-10-09T02:46:03.063 回答
2

在该特定示例中,第三个语句依赖于第一个和第二个语句,但第一个和第二个语句之间没有相互依赖关系。因此,运行时环境可以read-and-parse-a-number-from-console在与 不同的线程get-integer-from-web-service-call上执行,但第三条语句的执行必须等到前两条语句完成。

y * 4某些语言或运行时环境可能能够在获得 的实际值之前计算部分结果(例如) x。但是,作为高级程序员,您不太可能检测到这一点。

于 2012-10-08T20:21:55.490 回答