编写高质量代码改善C#程序的157个建议——建议83:小心Parallel中的陷阱

建议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个建议》陆敏技