建议83:小心Parallel中的陷阱
来源:互联网 发布:c 多文件编程问题 编辑:程序博客网 时间:2024/05/16 03:37
建议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个建议》陆敏技
- 建议83:小心Parallel中的陷阱
- 建议38:小心闭包中的陷阱
- 建议4:小心宏#define使用中的陷阱
- 改善C++ 程序的150个建议学习之建议14:小心typedef使用中的陷阱
- 改善C++ 程序的150个建议学习之建议4:小心宏#define使用中的陷阱
- 建议86:Parallel中的异常处理
- 小心笔试中的小陷阱(二)
- 小心笔试中的小陷阱(四)
- 小心笔试中的小陷阱(五)
- 小心oc中的回调陷阱
- 小心陷阱,少走弯路!游戏职场中21条的建议
- 小心笔试中的小陷阱(一)持续修改中~~
- 小心jstl标签、EL表达式中的空格陷阱
- 小心,StrConv有陷阱!
- 小心'溢出'陷阱
- 选购二手笔记本电脑小心陷阱
- 小心Comparator陷阱
- 小心陷阱--% of C
- 架构之路
- MYSQL自动备份策略的选择与实践
- CSU 1810: Reverse
- Android 资源名获取资源ID的两种实现方式-附带例子说明
- OPC实现方式-----OPC(第二篇)
- 建议83:小心Parallel中的陷阱
- leetcode围棋问题
- 如何解决万能地图下载器下载的地图和选择范围不一致
- 【读书笔记】JavaScript解决全局变量冲突
- 解决Java7下JTextPane不能自动换行的问题
- 瑞尼—记录事实真相的使者
- hibernate里的Transient注解的位置
- 文章标题
- Android AsyncTask完全解析,带你从源码的角度彻底理解