Практическое руководство. Написание цикла Parallel.ForEach c локальными переменными раздела

В следующем примере описывается порядок написания метода ForEach, использующего локальные переменные раздела. Когда выполняется цикл ForEach, он делит свою исходную коллекцию на несколько разделов. Каждый раздел содержит свою копию локальной переменной раздела. Локальная переменная раздела аналогична локальной переменной потока, однако допускает выполнение нескольких разделов в одном потоке.

Код и параметры в примере в значительной степени напоминают соответствующий метод For. См. дополнительные сведения о написании цикла Parallel.For с локальными переменными потока.

Чтобы использовать локальную переменную раздела в цикле ForEach, следует вызвать одну из перегрузок метода, принимающую два параметра типа. Первый параметр типа TSource указывает тип исходного элемента, а второй параметр типа TLocal указывает тип локальной переменной раздела.

Пример

В следующем примере выполняется вызов перегрузки Parallel.ForEach<TSource,TLocal>(IEnumerable<TSource>, Func<TLocal>, Func<TSource,ParallelLoopState,TLocal,TLocal>, Action<TLocal>) для вычисления суммы массива, состоящего из одного миллиона элементов. Эта перегрузка имеет четыре параметра:

  • source, который обозначает источник данных. Он должен реализовать IEnumerable<T>. Источником данных в нашем примере является объект миллионного члена IEnumerable<Int32>, возвращенный методом Enumerable.Range.

  • localInit или функция, инициализирующая локальную переменную раздела. Эта функция вызывается для каждого раздела, где выполняется операция Parallel.ForEach. В нашем примере локальная переменная раздела инициализируется с нулевым значением.

  • body — это делегат Func<T1,T2,T3,TResult>, который вызывается параллельным циклом на каждой итерации данного цикла. Он имеет сигнатуру Func\<TSource, ParallelLoopState, TLocal, TLocal>. Вы указываете код для делегата, а цикл передает следующие входные параметры:

    • Текущий элемент интерфейса IEnumerable<T>.

    • Переменная ParallelLoopState, которую можно использовать в коде делегата, чтобы определить состояние цикла.

    • Локальная переменная раздела.

    Ваш делегат возвращает локальную переменную раздела, которая затем передается в следующую итерацию цикла, выполняемого в данном конкретном разделе. Каждый раздел цикла сохраняет отдельный экземпляр данной переменной.

    В этом примере делегат добавляет значение каждого целого числа в локальную переменную раздела, где сохраняется промежуточная сумма значений целочисленных элементов в этом разделе.

  • localFinally, делегат Action<TLocal>, вызываемый Parallel.ForEach после завершения операций цикла в каждом разделе. Метод Parallel.ForEach передает вашему делегату Action<TLocal> окончательное значение локальной переменной раздела для данного раздела цикла, а вы указываете код, который выполняет требуемое действие для объединения результата из данного раздела с результатами из других разделов. Этот делегат может быть одновременно вызван несколькими задачами. В связи с этим в примере используется метод Interlocked.Add(Int32, Int32) для синхронизации доступа к переменной total. Поскольку делегат имеет тип Action<T>, возвращаемое значение отсутствует.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

class Test
{
    static void Main()
    {
        int[] nums = Enumerable.Range(0, 1000000).ToArray();
        long total = 0;

        // First type parameter is the type of the source elements
        // Second type parameter is the type of the thread-local variable (partition subtotal)
        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 += j; //modify local variable
                return subtotal; // value to be passed to next iteration
            },
            // Method to be executed when each partition has completed.
            // finalResult is the final value of subtotal for a particular partition.
            (finalResult) => Interlocked.Add(ref total, finalResult));

        Console.WriteLine("The total from Parallel.ForEach is {0:N0}", total);
    }
}
// The example displays the following output:
//        The total from Parallel.ForEach is 499,999,500,000
' How to: Write a Parallel.ForEach Loop That Has Thread-Local Variables

Imports System.Threading
Imports System.Threading.Tasks

Module ForEachThreadLocal
    Sub Main()

        Dim nums() As Integer = Enumerable.Range(0, 1000000).ToArray()
        Dim total As Long = 0

        ' First type parameter is the type of the source elements
        ' Second type parameter is the type of the thread-local variable (partition subtotal)
        Parallel.ForEach(Of Integer, Long)(nums, Function() 0,
                                           Function(elem, loopState, subtotal)
                                               subtotal += elem
                                               Return subtotal
                                           End Function,
                                            Sub(finalResult)
                                                Interlocked.Add(total, finalResult)
                                            End Sub)

        Console.WriteLine("The result of Parallel.ForEach is {0:N0}", total)
    End Sub
End Module
' The example displays the following output:
'       The result of Parallel.ForEach is 499,999,500,000

См. также