您的位置:首页 > 编程语言 > C#

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

2015-08-19 18:37 309 查看
建议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个建议》陆敏技
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: