在命令式编程中,for 循环可能是很容易并行化的最常见结构。循环的迭代器独立(independent)时,就可以在单独的线程上执行。就是说,由于独立,迭代器不会依赖前面任何迭代器所计算的值。
例如,统计数组中元素时,要计算下一个元素,就需要统计前面元素的总和。(这仍可以并行,但是不那么简单。)回想一下我们在第十章实现的“模糊”数组的函数,非常适合并行化:虽然每次迭代使用输入数组的多个元素,但它不依赖输出数组中的任何元素。清单 14.1 显示了基于前面示例的简单 for 循环,包含 C# 和 F# 两个版本。
清单14.1 用 for 循环计算模糊数组 (C# and F#)
C# F#
for(int i=1; i<inp.Length-1; i++){
var sum = inp[i-1] +
inp[i] + inp[i+1];
res[i] = sum / 3;
} for i in 1 .. inp.Length - 2 do
let sum = inp.[i-1] +
inp.[i] + inp.[i+1]
res.[i] <- sum / 3
虽然这是命令式代码,但仍可以成为纯函数式程序的一部分。Inp 是输入数组,在代码中的所有地方都不能修改,res 是输出数组,在循环计算后,也不应该进行修改。
要将循环并行化,可以使用 Parallel.For 方法。这个类在命名空间 System.Threading.Tasks 下,只在 .NET 4.0 上才可用。Parallel.For 方法的参数为 Action 委托值,可以由 lambda 函数提供。F# 中,直接使用这个方法感觉有点大材小用,所以,我们会定义一个简单的函数,使代码更简洁:
let pfor nfrom nto f =
Parallel.For(nfrom, nto + 1, Action<_>(f)) |> ignore
这段代码把函数 f (其类型为 int -> unit)包装在委托类型,运行并行的 for 循环。方法返回循环是否成功完成的信息,但我们并不需要,所以就忽略了。注意,我们还在上限上加了 1,因为在 F# 的 for 循环是包容上限的,而在 C# for 循环和 Parallel.For 方法中是不包含上限的。清单 14.2 显示前面示例的并行化版本。
清单14.2 并行折 for 循环 (C# and F#)
C# F#
Parallel.For(1,inp.Length-1,i => {
var sum = inp[i-1] +
inp[i] + inp[i+1];
res[i] = sum / 3;
}); pfor 1 (inp.Length-2) (fun i –>
let sum = inp.[i-1] +
inp.[i] + inp.[i+1]
res.[i] <- sum / 3
)
可以发现,这与原始的串行版本几乎一样简单。另外,还表明了函数式构造的强大:由于有 lambda 函数,把串行的 for 循环转换为并行时,唯一要做的是使用 Parallel.For 方法(或在 F# 中使用 pfor 函数),代替内置的语言构造。
注意
在 Parallel 类中,除了有 For 方法,还有 ForEach 方法,可用于对 C# 中的 foreach 构造,或 F# 中的 for …in … do 构造进行并行化。这两个方法都可以重载,自定义迭代器。重载可以改变步长,增加 For 方法中的索引,或者停止并行执行(类似于 C# 中的循环中断break)。如果觉得需要更多一点儿控制,就请参考文档,看看其中是否有重载可以帮助你。
处理数组和其他命令式数据结构,Parallel.For 方法特别有用。在这一章的后面(第 14.2.5 节),我们会在更大的示例应用程序中再次用它来以函数方式处理数组。现在,就先结束介绍,下面要讨论的两种技术都是纯函数式的。