建议83:小心Parallel中的陷阱
Parallel的For和ForEach方法还支持一些相对复杂的应用。在这些应用中,它允许我们在每个任务启动时执行一些初始化操作,在每个任务结束后,又执行一些后续工作,同时,还允许我们监视任务的状态。但是,记住上面这句话“允许我们监视任务的状态”是错误的:应该把其中的“任务”改成“线程”。这,就是陷阱所在。
我们需要深刻理解这些具体的操作和应用,不然,极有可能陷入这个陷阱中去。下面体会这段代码的输出是什么,如下所示:
static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total = 0; Parallel.For<int>(0, nums.Length, () => { return 1; }, (i, loopState, subtotal) => { subtotal += nums[i]; return subtotal; }, (x) => Interlocked.Add(ref total, x) ); Console.WriteLine("total={0}", total); Console.ReadKey(); }
这段代码有可能输出11,较少的情况下输出12,虽然理论上有可能输出13和14,但是我们应该很少有机会观察到。要明白为什么会有这样的输出,首先必须详细了解For方法的各个参数。上面这个For方法的声明如下:
public static ParallelLoopResult For<TLocal>(int fromInclusive, int toExclusive, Func<TLocal> localInit, Func<int, ParallelLoopState, TLocal, TLocal> body, Action<TLocal> localFinally);
前面两个参数相对容易理解,分别是起始索引和结束索引。
参数body也比较容易理解,即任务体本身。其中subtotal为单个任务的返回值。
localInit和localFinally就比较难理解了,并且陷阱也在这里。要理解这两个参数,必须先理解Parallel.For方法的运作模式。For方法采用并发的方式来启动循环体中的每个任务,这意味着,任务是交给线程池去管理的。在上面的代码中,循环次数共计4次,实际运行时调度启动的后台线程也就只有一个或两个。这就是并发的优势,也是线程池的优势,Parallel通过内部的调度算法,最大化地节约了线程的消耗。localInit的作用是如果Parallel为我们新起了一个线程,它就会执行一些初始化的任务在上面的例子中:
() => { return 1; }
它会将任务体中的subtotal这个值初始化为1。
localFinally的作用是,在每个线程结束的时候,它执行一些收尾工作:
(x) => Interlocked.Add(ref total, x)
这行代码所代表的收尾工作实际就是:
totaltotal = total + subtotal;
其中的x,其实代表的就是任务体中的返回值,具体在这个例子中就是subtotal在返回时的值。使用Interlocked是对total使用原子操作,以避免并发所带来的问题。
现在,我们应该很好理解为什么上面这段代码的输出会不确定了。Parallel一共启动了4个任务,但是我们不能确定Parallel到底为我们启动了多少个线程,那是运行时根据自己的调度算法决定的。如果所有的并发任务只用了一个线程,则输出为11;如果用了两个线程,那么根据程序的逻辑来看,输出就是12了。
在这段代码中,如果让localInit返回的值为0,也许你就永远不会注意到这个陷阱:
() => { return 0; }
现在,为了更清晰地体会这个陷阱,我们使用下面这段更好理解的代码:
static void Main(string[] args) { string[] stringArr = new string[] { "aa", "bb", "cc", "dd", "ee", "ff", "gg", "hh" }; string result = string.Empty; Parallel.For<string>(0, stringArr.Length, () => "-", (i, loopState, subResult) => { return subResult += stringArr[i]; }, (threadEndString) => { result += threadEndString; Console.WriteLine("Inner:" + threadEndString); }); Console.WriteLine(result); Console.ReadKey(); }
这段代码的一个可能的输出为:
Inner:-aaccddeeffgghh
Inner:-bb
-aaccddeeffgghh-bb
转自:《编写高质量代码改善C#程序的157个建议》陆敏技