Параллельное программирование в C#: класс Parallel

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

Класс Parallel входит в состав библиотеки TPL .NET и позволяет достаточно легко (даже для неопытного разработчика) распараллелить ряд задач. В этом классе содержится ряд методов, обеспечивающих, в том числе, параллельное выполнение действий (Action) о которых речь шла в прошлой статье про TPL и параллельное выполнение итераций циклов for и foreach.

Метод Parallel.Invoke

Статический метод класса Parallel.Invoke() позволяет выполнить параллельно массив действий (Action). Например, запустим параллельное выполнение трех действий: расчёт факториала, определение простых чисел и вывод строки в консоль:

class Program
{

    static void Factorial(int x)
    {
        Console.WriteLine($"Запускаем расчёт факториала числа {x}. CurrentId = {Task.CurrentId}");
        int result = 1;
        for (int i = 1; i <= x; i++)
            result *= i;
        Console.WriteLine($"Расчёт факториала выполнен. Результат {result}");  
    }

    static void Prime(int x)
    {
        Console.WriteLine($"Запускаем расчёт простых чисел от 2 до {x}. CurrentId = {Task.CurrentId}");
        int total = 0;
        for (int i = 2; i <= x; i++)
        {
            bool isPrime = true;
            for (int j = 2; j < i; j++)
            {
                if (i % j == 0)
                {
                    isPrime = false;
                    break;
                }
            }    
                
            if (isPrime)
                total++;
        }
        Console.WriteLine($"Расчёт простых чисел выполнен. Найдено {total} чисел");
    }

    
    static void Main(string[] args)
    {


        Parallel.Invoke(() => Factorial(10), //первая задача
                        () => Prime(5000),   //вторая задача
                        () => Console.WriteLine("Просто выводим строку на экран") //третья задача
                        );
        Console.ReadLine();
        
    }
}

Вывод консоли может быть следующим:

Запускаем расчёт факториала числа 10. CurrentId = 3

Расчёт факториала выполнен. Результат 3628800

Запускаем расчёт простых чисел от 2 до 5000. CurrentId = 1

Просто выводим строку на экран

Расчёт простых чисел выполнен. Найдено 669 чисел

Как мы уже знаем, пока мы не определим самостоятельно порядок выполнения параллельных вычислений, TPL нам не гарантирует, что задачи будут выполняться в том порядке, в котором они были определены. Вместе с этим, при использовании метода Parallel.Invoke() TPL сама возьмет на себя такие моменты как распределение задач по ядрам процессора, определение максимального числа потоков и так далее. У этого метода так же есть одна перегруженная версия, позволяющая производить настройку выполнения задач, а также определить максимальное количество одновременно выполняемых задач:

public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);

Метод Parallel.For

Статические метод Parallel.For позволяет выполнять итерации цикла for параллельно. В самом простом варианте, сигнатура метода выглядит следующим образом:

public static ParallelLoopResult For(int fromInclusive, int toExclusive, Action<int> body);

Здесь fromInclusive — это начальное значение счётчика цикла, toExclusive — конечное значение счётчика цикла, body — тело цикла (делегат Action). В качестве результата, этот метод возвращает структуру, содержащую следующую информацию:

public struct ParallelLoopResult
{
    public bool IsCompleted { get; }
    public long? LowestBreakIteration { get; }  
}

IsCompleted — указывает, выполнен ли цикл успешно или был прерван; LowestBreakIteration — минимальный номер итерации цикла где был вызван оператор break.

Чтобы продемонстрировать работу метода Parallel.For, вернемся к нашему методу определения простых чисел. Сейчас он выглядит следующим образом:

static void Prime(int x)
{
    Console.WriteLine($"Запускаем расчёт простых чисел от 2 до {x}. CurrentId = {Task.CurrentId}");
    int total = 0;
    for (int i = 2; i <= x; i++)
    {
        bool isPrime = true;
        for (int j = 2; j < i; j++)
        {
            if (i % j == 0)
            {
                isPrime = false;
                break;
            }
        }    
            
        if (isPrime)
            total++;
    }
    Console.WriteLine($"Расчёт простых чисел выполнен. Найдено {total} чисел");
}

Сделаем следующее:

  1. Внешний цикл for будет выполняться параллельно (с использованием метода Parallel.For)
  2. Внутренний цикл for будет выводить простые числа в консоль и, по сути, являться телом цикла Parallel.For.

Так будет выглядеть определение простого числа (тело цикла Parallel.For):

static public void IsPrime (int x)
    {
       bool isPrime = true;
       for (int i = 2; i < x; i++)
       {
        if (x % i == 0)
        {
            isPrime = false;
            break;
        }
    }
    if (isPrime)
        Console.WriteLine(x);
}

А вот так будет вызываться метод Parallel.For:

Parallel.For(2, 5001, IsPrime);

То есть цикл будет стартовать с 2 и заканчиваться на значении 5000 включительно. При этом, наш метод  IsPrime принимает в качестве параметра целое число int (номер итерации цикла for). Таким образом, мы можем легко распараллелить выполнение циклов в нашей программе.

Метод Parallel.ForEach

Этот метод выполняет цикл foreach в параллельном режиме.  Сигнатура метода следующая:

public static ParallelLoopResult ForEach<TSource>(IEnumerable<TSource> source, Action<TSource> body);

Чтобы воспользоваться этим методом, снова вернемся к нашему определению простых чисел. Вот так, например, можно определить простые числа из заданного набора, а не по порядку от 2 до x, как в предыдущем примере:

Parallel.ForEach<int>(new List<int> { 10, 12, 2, 3, 4, 5, 6, 7, 89, 198 }, IsPrime);

Это простой пример, хотя,никто не запрещает вам использовать в методе Parallel.ForEach коллекции не только простых типов данных, но и, например, собственных классов.

Выход из цикла

В обычных циклах for и foreach предусмотрены специальные операторы управления циклом, в частности, оператор break для выхода из цикла. При использовании класса Parallel у нас также есть возможность выйти из цикла на любой его итерации. Например, выйдем из цикла foreach при нахождении первого простого числа в списке List.

Перепишем метод IsPrime следующим образом:

static public void IsPrime (int x, ParallelLoopState parallelLoopState)
    {
       bool isPrime = true;
       for (int i = 2; i < x; i++)
       {
        if (x % i == 0)
        {
            isPrime = false;
            break;
        }
    }
    if (isPrime)
    {
        Console.WriteLine($"Первое найденное простое число {x}");
        parallelLoopState.Break();
    }
        
}

Теперь наш метод в качестве второго параметра получает объект ParallelLoopState, который позволяет управлять циклом. Как только мы находим в очередной итерации цикла простое число, то при первом же удобном случае вызывается метод Break прекращающий выполнение цикла. При этом, следует обратить внимание на то, что вызов Break не означает, что цикл прекратиться немедленно, т.к. в момент вызова Break параллельно могут рассчитываться и другие итерации цикла. Например, попробуем вызвать Parallel.ForEach, используя наш метод IsPrime следующим образом:

ParallelLoopResult res = Parallel.ForEach<int>(new List<int> { 10, 12, 2, 3, 4, 5, 6, 7, 89, 198 }, IsPrime);
if (res.IsCompleted)
    Console.WriteLine("Цикл выполнен полностью");
Console.WriteLine($"Номер итерации на которой сработал break - {res.LowestBreakIteration}");

В результате, мы можем увидеть в консоли следующие строки:

Первое найденное простое число 7

Первое найденное простое число 5

Первое найденное простое число 89

Первое найденное простое число 2

Первое найденное простое число 3

Номер итерации на которой сработал break — 2

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

Итого

Класс Parallel библиотеки TPL позволяет достаточно легко и удобно выполнять параллельные задачи в приложении. Метод Invoke позволяет запустить параллельное выполнение массива задач, а методы For и ForEach позволяют организовать параллельное выполнение итераций цикла.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
guest
0 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии