Параллелизм, многопоточность, асинхронность: разница и примеры применения (.NET, C#)

Апрель 2021
~4 мин
Все публикации
Тимур Гайфулин, руководитель группы разработки digital-интегратора DD Planet
Источник: Tproger

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Конструкция 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

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

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

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

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

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

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

Найдем решение вашей задачи

Заполнить бриф
Форматы: jpg, png, xsl, PDF, doc. Размер до 2 МБ
Нажимая кнопку «Отправить», Вы принимаете условия обеспечения конфиденциальности персональных данных.
Отправить