Защита потока от возврата урожая с помощью Parallel.ForEach()

Рассмотрим следующий пример кода, который создает перечислимый набор целых чисел и обрабатывает его параллельно:

using System.Collections.Generic;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        Parallel.ForEach(CreateItems(100), item => ProcessItem(item));
    }

    private static IEnumerable<int> CreateItems(int count)
    {
        for (int i = 0; i < count; i++)
        {
            yield return i;
        }
    }

    private static void ProcessItem(int item)
    {
        // Do something
    }
}

Гарантировано ли, что рабочие потоки, сгенерированные с помощью Parallel.ForEach(), получат другой элемент или какой-то механизм блокировки вокруг приращения и требуется возврат i?

Ответ 1

Parallel.ForEach<TSource>, когда TSource является IEnumerable<T>, создает разделитель для IEnumerable<T>, который включает в себя собственный механизм внутреннего блокирования, поэтому вам не нужно реализовывать какую-либо безопасность потоков в вашем итераторе.

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

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

Как вы видите, прогон через IEnumerable<T> для целей секционирования является последовательным (доступ осуществляется через общую блокировку), а разделы обрабатываются параллельно.

Ответ 2

TPL и PLINQ используют концепцию разделителей.

Partitioner - это тип, который наследует Partitioner<TSource> и служит для разделения исходной последовательности на числовые части (или разделы). Встроенные разделители были предназначены для разделения исходной последовательности на неперекрывающиеся разделы.