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

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

Пример

В следующем примере выполняется вызов метода For<TLocal>(Int32, Int32, Func<TLocal>, Func<Int32,ParallelLoopState,TLocal,TLocal>, Action<TLocal>) для вычисления суммы значений в массиве, содержащем один миллион элементов. Значение каждого элемента равно его индексу.

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

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

        // Use type parameter to make subtotal a long, not an int
        Parallel.For<long>(0, nums.Length, () => 0,
            (j, loop, subtotal) =>
            {
                subtotal += nums[j];
                return subtotal;
            },
            subtotal => Interlocked.Add(ref total, subtotal));

        Console.WriteLine("The total is {0:N0}", total);
        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
    }
}
'How to: Write a Parallel.For Loop That Has Thread-Local Variables

Imports System.Threading
Imports System.Threading.Tasks

Module ForWithThreadLocal

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

        ' Use type parameter to make subtotal a Long type. Function will overflow otherwise.
        Parallel.For(Of Long)(0, nums.Length, Function() 0, Function(j, [loop], subtotal)
                                                                subtotal += nums(j)
                                                                Return subtotal
                                                            End Function, Function(subtotal) Interlocked.Add(total, subtotal))

        Console.WriteLine("The total is {0:N0}", total)
        Console.WriteLine("Press any key to exit")
        Console.ReadKey()
    End Sub

End Module

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

Третий параметр имеет тип Func<TResult>, где TResult — это тип переменной для сохранения локального состояния потока. Этот тип определяется аргументом универсального типа, переданным во время вызова универсального метода For<TLocal>(Int32, Int32, Func<TLocal>, Func<Int32,ParallelLoopState,TLocal,TLocal>, Action<TLocal>), в данном случае он равен Int64. Этот аргумент типа сообщает компилятору тип временной переменной, которая будет использоваться для хранения локального состояния в потоке. В этом примере выражение () => 0 (или Function() 0 в Visual Basic) инициализирует локальную переменную потока с нулевым значением. Если бы аргумент универсального типа имел ссылочный тип или тип пользовательского значения, выражение выглядело бы следующим образом:

() => new MyClass()  
Function() new MyClass()  

Четвертый параметр определяет логическую схему цикла. Он должен быть делегатом или лямбда-выражением, сигнатура которого имеет значение Func<int, ParallelLoopState, long, long> в C# или Func(Of Integer, ParallelLoopState, Long, Long) в Visual Basic. Первый параметр представляет собой значение счетчика цикла для данной итерации цикла. Второй является объектом ParallelLoopState, который можно использовать для выхода из цикла; данный объект предоставляется классом Parallel каждому экземпляру цикла. Третий параметр представляет собой локальную переменную потока. Последний параметр является типом возвращаемого значения. В данном случае этот тип имеет значение Int64, так как именно его мы указали в аргументе типа For. Эта переменная называется subtotal и возвращается лямбда-выражением. Возвращаемое значение используется для инициализации subtotal на каждой последующей итерации цикла. Этот последний параметр также можно представить себе в виде значения, которое передается в каждую итерацию, а после выполнения последней итерации передается в делегат localFinally.

Пятый параметр задает метод, который вызывается один раз после выполнения всех итераций в определенном потоке. Тип входного аргумента опять соответствует аргументу типа метода For<TLocal>(Int32, Int32, Func<TLocal>, Func<Int32,ParallelLoopState,TLocal,TLocal>, Action<TLocal>) и типу, возвращаемому телом лямбда-выражения. В данном примере это значение добавляется в переменную в области видимости класса потокобезопасным образом путем вызова метода Interlocked.Add. Благодаря использованию локальной переменной потока нам не пришлось выполнять запись в эту переменную класса при каждой итерации цикла.

См. дополнительные сведения о лямбда-выражениях в PLINQ и TPL.

См. также