Parallel ForEach的本地初始化如何工作?

时间:2013-02-12 11:16:19

标签: c# task-parallel-library

我不确定如何在Parallel.ForEach中使用本地init函数,如msdn文章中所述:http://msdn.microsoft.com/en-us/library/dd997393.aspx

Parallel.ForEach<int, long>(nums, // source collection
   () => 0, // method to initialize the local variable
   (j, loop, subtotal) => // method invoked by the loop on each iteration
   {
      subtotal += nums[j]; //modify local variable 
      return subtotal; // value to be passed to next iteration
   },...

()=&gt; 0初始化什么?变量的名称是什么以及如何在循环逻辑中使用它?

4 个答案:

答案 0 :(得分:25)

参考Parallel.ForEach静态扩展方法的following overload

public static ParallelLoopResult ForEach<TSource, TLocal>(
    IEnumerable<TSource> source,
    Func<TLocal> localInit,
    Func<TSource, ParallelLoopState, TLocal, TLocal> taskBody,
    Action<TLocal> localFinally
)

在您的具体示例中

该行:

() => 0, // method to initialize the local variable

只是一个lambda(匿名函数),它将返回常量整数零。这个lambda作为localInit参数传递给Parallel.ForEach - 因为lambda返回一个整数,它的类型为Func<int>,类型TLocal可以推断为int由编译器(类似地,TSource可以从作为参数source传递的集合的类型推断出来)

然后将返回值(0)作为第3个参数(名为subtotal)传递给taskBody Func。这个(0)用于身体循环的初始种子:

(j, loop, subtotal) =>
{
    subtotal += nums[j]; //modify local variable (Bad idea, see comment)
    return subtotal;     // value to be passed to next iteration
}

第二个lambda(传递给taskBody)被称为N次,其中N是TPL分区器分配给该任务的项目数。

对第二个taskBody lambda的每次后续调用都将传递subTotal的新值,有效地计算此任务的正在运行的部分总计。在添加了分配给此任务的所有项目之后,将再次调用第三个和最后一个localFinally函数参数,并传递从subtotal返回的taskBody的最终值。由于几个这样的任务将并行运行,因此还需要最后一步将所有部分总数加入到最终的“盛大”中。总。但是,因为多个并发任务(在不同的线程上)可能会争用grandTotal变量,所以对它的更改必须以线程安全的方式完成。

(我已更改MSDN变量的名称以使其更清晰)

long grandTotal = 0;
Parallel.ForEach(nums,            // source collection
  () => 0,                        // method to initialize the local variable
  (j, loop, subtotal) =>          // method invoked by the loop on each iteration
     subtotal + nums[j],          // value to be passed to next iteration subtotal
  // The final value of subtotal is passed to the localFinally function parameter
  (subtotal) => Interlocked.Add(ref grandTotal, subtotal)

在MS示例中,修改任务主体内部的参数小计是一种不好的做法,而且是不必要的。即代码subtotal += nums[j]; return subtotal;会更好,因为return subtotal + nums[j];可以缩写为lambda速记投影(j, loop, subtotal) => subtotal + nums[j]

一般

Parallel.For / Parallel.ForEachlocalInit / body / localFinally重载允许每次执行任务初始化和清理代码,分别在taskBody次迭代之前和之后运行由任务执行。

(注意传递给并行For / Foreach的For range / Enumerable将被分区为IEnumerable<>批,其中每个都将分配一个任务)

每个任务中,localInit将被调用一次,body代码将被重复调用,每个项目批量调用一次(0..N次),完成后将调用localFinally

此外,您可以通过{{1}的通用taskBody返回值传递任务期间所需的任何状态(即localFinallyTLocal委托)我在下面调用了这个变量localInit Func

&#34; localInit&#34;的常用用途:

  • 创建和初始化循环体所需的昂贵资源,如数据库连接或Web服务连接。
  • 保持任务 - 本地变量保持(无竞争)运行总计或集合
  • 如果您需要将多个对象从taskLocals返回到localInittaskBody,则可以使用强类型类localFinally或者,如果您对Tuple<,,>只使用lambdas,你也可以通过匿名类传递数据。请注意,如果您使用localInit / taskBody / localFinally的返回值来共享多个任务中的引用类型,则需要考虑此对象的线程安全性 - 不可变性。

&#34; localFinally&#34;的常用用途动作:

  • 发布localInit中使用的IDisposables等资源(例如数据库连接,文件句柄,Web服务客户端等)
  • 将每个任务完成的工作聚合/组合/减少回共享变量。这些共享变量将被争用,因此线程安全性是一个问题:
    • e.g。原始类型的taskLocals,如整数
    • 写操作需要
    • Interlocked.Increment或类似的
    • 利用concurrent collections来节省时间和精力。

lock是循环操作的taskBody部分 - 您希望针对性能优化此功能。

最好用评论的例子总结这一点:

tight

还有更多例子:

Example of per-Task uncontended dictionaries

Example of per-Task database connections

答案 1 :(得分:7)

作为@Honza Brestan答案的延伸。 Parallel foreach将工作分成任务的方式也很重要,它会将几个循环迭代分组到一个任务中,因此在实践中localInit()会在循环的每n次迭代中调用一次,并且可以同时启动多个组。

localInitlocalFinally的要点是确保并行的foreach循环可以将每次迭代的结果合并为单个结果,而无需在body中指定锁定语句要执行此操作,您必须为要创建的值(localInit)提供初始化,然后每个body itteration可以处理本地值,然后您提供一种方法来组合每个组的值({ {1}})以线程安全的方式。

如果您不需要localInit来同步任务,您可以使用lambda方法正常引用周围上下文中的值而不会出现任何问题。有关使用localInit / Finally的更深入的教程以及向下滚动到使用本地值进行优化,请参阅Threading in C# (Parallel.For and Parallel.ForEach),Joseph Albahari确实是我所有线程的goto源代码。

答案 2 :(得分:2)

您可以在 correct Parallel.ForEach 重载中获得有关MSDN的提示。

  

对参与循环执行的每个线程调用一次localInit委托,并为每个任务返回初始本地状态。这些初始状态将传递给每个任务的第一个正文调用。然后,每个后续的主体调用都会返回一个可能已修改的状态值,该值将传递给下一个主体调用。

在您的示例中,() => 0是一个仅返回0的委托,因此该值用于每项任务的第一次迭代。

答案 3 :(得分:0)

从我这边稍微简单一点的例子

class Program
{
    class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }

    static List<Person> GetPerson() => new List<Person>()
    {
        new Person() { Id = 0, Name = "Artur", Age = 26 },
        new Person() { Id = 1, Name = "Edward", Age = 30 },
        new Person() { Id = 2, Name = "Krzysiek", Age = 67 },
        new Person() { Id = 3, Name = "Piotr", Age = 23 },
        new Person() { Id = 4, Name = "Adam", Age = 11 },
    };

    static void Main(string[] args)
    {
        List<Person> persons = GetPerson();
        int ageTotal = 0;

        Parallel.ForEach
        (
            persons,
            () => 0,
            (person, loopState, subtotal) => subtotal + person.Age,
            (subtotal) => Interlocked.Add(ref ageTotal, subtotal)
        );

        Console.WriteLine($"Age total: {ageTotal}");
        Console.ReadKey();
    }
}