• 【转】编写高质量代码改善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个建议》陆敏技

  • 相关阅读:
    461. Hamming Distance
    Myeclipse中WebServlet cannot be resolved to a type报错
    注解方式配置Servlet(Servlet3.0)
    Oracle连接池操作
    最短路径算法
    编写学生类Stu
    编写程序,统计某旅馆住宿客人的总数,要求输入客人姓名,输出客人编号(按先后顺序自动生成),姓名以及总人数。
    货物管理系统
    c# 利用动态库DllImport("kernel32")读写ini文件(提供Dmo下载)
    shut
  • 原文地址:https://www.cnblogs.com/farmer-y/p/7993823.html
Copyright © 2020-2023  润新知