Параллелизм, многопоточность, асинхронность

Многие начинающие специалисты путают многопоточное. Асинхронное и параллельное программирование. На первый взгляд. Может показаться. Что это одно и то же — но нет. Тимур Гайфулин, руководитель группы разработки digital-интегратора DD Planet, совместно с Алексеем Гришиным, ведущим разработчиком DD Planet. Попробовал разобраться. Сколько программных моделей используют C#-разработчики и в чём их отличия. Статью опубликовал сайт tproger.ru.

Существует несколько концепций: синхронное/асинхронное программирование и однопоточные/многопоточные приложения. Причём первая программная модель может работать в однопоточной или многопоточной среде. То есть приложение может быть: синхронным однопоточным. Синхронным многопоточным и асинхронным многопоточным.

Отдельной концепцией считается параллелизм. Который является подмножеством многопоточного типа приложений. Рассмотрим особенности каждой программной модели подробнее.

Синхронная модель

Потоку назначается одна задача. И начинается её выполнение. Заняться следующей задачей можно только тогда. Когда завершится выполнение первой. Эта модель не предполагает приостановку одной задачи. Чтобы выполнить другую.

Однопоточность

Система в одном потоке работает со всеми задачами. Выполняя их поочерёдно.

Однопоточная синхронная система

Многопоточность

В этом случае речь о нескольких потоках. В которых выполнение задач идет одновременно и независимо друг от друга.

Многопоточная синхронная система

Пример такого концепта — одновременная разработка веб- и мобильного приложений и серверной части. При условии соблюдения архитектурных «контрактов».

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

Асинхронность

Характеристики асинхронного кода:

  • обрабатывает больше запросов сервера. Предоставляя потокам возможность обрабатывать больше запросов во время ожидания результата от запросов ввода-вывода;
  • делает пользовательский интерфейс быстрым. Выделяя потоки для обработки действий в пользовательском интерфейсе во время ожидания запросов ввода-вывода. Передавая затратные по времени операции другим ядрам ЦП.

Если у системы много потоков. То их асинхронная работа выглядит примерно так:

Многопоточная асинхронная система

Конструкция async/await

Для работы с асинхронными вызовами в C# необходимы два ключевых слова:

  • async — используется в заголовке метода;
  • await — вызывающий метод содержит одно или несколько таких выражений.

Они используются вместе для создания асинхронного метода. У асинхронных методов могут быть следующие типы возвращаемых значений:

  1. Task для асинхронного метода. Который выполняет операцию. Но не возвращает значение;
  2. Task<TResult> для асинхронного метода. Возвращающего значение;
  3. void для обработчика событий;
  4. начиная с версии 7.0 в языке C# поддерживаются любые типы с доступным методом GetAwaiter;
  5. начиная с версии 8.0 в языке C# поддерживается интерфейс IAsyncEnumerable<T> для асинхронного метода. Который возвращает асинхронный поток.

Сама конструкция async/await появилась в C# 5.0 с выходом .NET Framework 4.5 и отчасти представляет собой синтаксический сахар. Механизм async/await не имеет реализации в CLR и разворачивается компилятором в сложную конструкцию на IL. Но эта конструкция — не сахар вокруг тасок. А отдельный механизм. Использующий класс Task для переноса состояния исполняемой части кода.

Пример асинхронного метода:

using System;
using System.Threading;
using System.Threading.Tasks; namespace FactorialApp
{ class Program { static void Factorial() { int result = 1; for (int i = 1; i <= 6; i++) { result *= i; } Thread.Sleep(8000); Console.WriteLine($"Факториал равен {result}"); } // определение асинхронного метода static async void FactorialAsync() { Console.WriteLine("Начало метода FactorialAsync"); // выполняется синхронно await Task.Run(() => Factorial()); // выполняется асинхронно Console.WriteLine("Конец метода FactorialAsync"); } static void Main(string[] args) { FactorialAsync(); // вызов асинхронного метода Console.WriteLine("Введите число: "); int n = Int32.Parse(Console.ReadLine()); Console.WriteLine($"Квадрат числа равен {n * n}"); Console.Read(); } }
}
Результат асинхронного вычисления факториала

Этот пример приведён лишь для наглядности. Особого смысла делать логику вычисления факториала асинхронной нет. Опять же, для имитации долгой работы мы использовали задержку на 8 секунд с помощью методы Thread.Sleep(). Цель была показать: асинхронная задача. Которая может выполняться долгое время. Не блокирует основной поток — в этом случае метод Main(). И мы можем вводить и обрабатывать данные. Продолжая работу с ним.

Параллелизм

Эта программная модель подразумевает. Что задача разбивается на несколько независимых подзадач. Которые можно выполнить параллельно. А затем объединить результаты. Примером такой задачи может быть Parallel LINQ:

IEnumerable yourData = GetYourData();
var result = yourData.AsParallel() // начинаем обрабатывать параллельно
.Select(d => d.CalcAmount()) // Вычисляем параллельно
.Where(amount => amount > 0)
.ToArray(); // Возвращаемся к синхронной модели
Обзор архитектуры параллельного программирования в .NET

Еще один пример — вычисление среднего значения двумерного массива. Когда каждый отдельный поток может подсчитать сумму своей строки. А потом объединить результат и вычислить среднее.

Однако не стоит забывать. Что не все задачи поддаются распараллеливанию. Например, описанная выше задача по вычислению факториала. В которой на каждом последующем этапе нужен результат предыдущего.

Какую программную модель выбрать?

Перечисленные программные модели должны применяться в зависимости от задач. Их можно использовать как отдельно во всём приложении. Так и сочетать между собой. Главное, чтобы приложение было максимально эффективным и удовлетворяло требования пользователя.

Если речь идет о сложных многопользовательских приложениях. То стремиться стоит к использованию асинхронной модели. Так как важна интерактивность и отзывчивость интерфейса. Взаимодействие с пользователем в активном режиме всегда должно быть максимально эффективным. Даже если в фоновом режиме в то же время выполняются другие задачи. Издержки асинхронности, например. На переключение исполняемого контекста. В таком случае нивелируются за счет общей эффективности приложения.

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