Защита потока от возврата урожая с помощью 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>
и служит для разделения исходной последовательности на числовые части (или разделы). Встроенные разделители были предназначены для разделения исходной последовательности на неперекрывающиеся разделы.