Параллелизм против многопоточности против асинхронного программирования: разъяснение
В последние время, я выступал на мероприятиях и отвечал на вопрос аудитории между моими выступлениями о Асинхронном программировании, я обнаружил что некоторые люди путали многопоточное и асинхронное программирование, а некоторые говорили, что это одно и тоже. Итак, я решил разъяснить эти термины и добавить еще одно понятие Параллелизм. Здесь есть две концепции и обе они совершенно разные, первая синхронное и асинхронное программирование и вторая – однопоточные и многопоточные приложения. Каждая программная модель (синхронная или асинхронная) может работать в однопоточной и многопоточной среде. Давайте обсудим их подробно.
Синхронная программная модель – это программная модель, когда потоку назначается одна задача и начинается выполнение. Когда завершено выполнение задачи тогда появляется возможность заняться другой задачей. В этой модели невозможно останавливать выполнение задачи чтобы в промежутке выполнить другую задачу. Давайте обсудим как эта модель работает в одно и многопоточном сценарии.
Однопоточность – если мы имеем несколько задач, которые надлежит выполнить, и текущая система предоставляет один поток, который может работать со всеми задачами, то он берет поочередно одну за другой и процесс выглядит так:
Здесь мы видим, что мы имеем поток (Поток 1) и 4 задачи, которые необходимо выполнить. Поток начинает выполнять поочередно одну за одной и выполняет их все. (Порядок, в котором задачи выполняются не влияет на общее выполнение, у нас может быть другой алгоритм, который может определять приоритеты задач.
Многопоточность – в этом сценарии, мы использовали много потоков, которые могут брать задачи и приступать к работе с ними. У нас есть пулы потоков (новые потоки также создаются, основываясь на потребности и доступности ресурсов) и множество задач. Итак, поток может работать вот так:
Здесь мы можем видеть, что у нас есть 4 потока и столько же задач для выполнения, и каждый поток начинает работать с ними. Это идеальный сценарий, но в обычных условиях мы используем большее количество задач чем количество доступных потоков, таким образом освободившийся поток получает другое задание. Как уже говорилось создание нового потока не происходит каждый раз потому что для этого требуются системные ресурсы такие как процессор, память и начальное количество потоков должно быть определенным.
Теперь давайте поговорим о Асинхронной модели и как она ведет себя в одно и многопоточной среде.
Асинхронная модель программирования – в отличии от синхронной программной модели, здесь поток однажды начав выполнение задачи может приостановить выполнение сохранив текущее состояние и между тем начать выполнение другой задачи.
Здесь мы можем видеть, что один поток отвечает за выполнение всех задач и задачи чередуются друг за другом.
Если наша система способно иметь много потоков тогда все потоки могут работать в асинхронной модели как показано ниже:
Здесь мы можем видеть, что одна и та же задача скажем Т4, Т5, Т6 … обрабатывается несколькими потоками. Это красота этого сценария. Как мы можем видеть, что задача Т4 начала выполнение первой Потоком 1 и завершен Потоком 2. Подобным образом задча Т6 выполнена Потоком 2, Потоком 3 и Потоком 4. Это демонстрирует максимальное использование потоков.
Итак, до сих пор мы обсудили 4 сценария:
- Синхронный однопоточный
- Синхронный многопоточный
- Асинхронный однопоточный
- Асинхронный многопоточный
Проще говоря параллелизм способ обработки множественных запросом одновременно. Так как мы обсуждали два сценария когда обрабатывались множественные запросы, многопоточное программирование и асинхронная модель (одно и многопоточная). В случае асинхронной модели будь она однопоточной или многопоточной, в то время, когда выполняются множество задач, некоторые из них приостанавливаются, а некоторые выполняются. Существует много особенностей, но это выходит за рамки этой публикации.
Как обсуждалось ранее новая эпоха за асинхронным программированием. Почему это так важно?
Преимущества асинхронного программирования
Существует две вещи очень важные для каждого приложения – удобство использования и производительность. Удобство использования потому что пользователь нажав кнопку чтобы сохранить некоторые данные что в свою очередь требует выполнения множества задач таких как чтение и заполнение данных во внутреннем объекте, установление соединения с SQL и сохранения его там. В свою очередь SQL запускается на другой машине в сети и работает под другим процессом, это может потребовать много время. Таким образом если запрос обрабатывается одним процессом экран будет находится в зависшем состоянии до тех пор, пока процесс не завершится. Вот почему сегодня многие приложения и фреймворки полностью полагаются на асинхронную модель.
Производительность приложения и системы также очень важны. Было замечено в то время как выполняется запрос, около 70-80% из них попадают в ожидании зависимых задач. Таким образом, это может быть максимально использовано в асинхронном программирование, где, как только задача передается другому потоку (например, SQL), текущий поток сохраняет состояние и доступен для выполнения другого процесса, а когда задача sql завершается, любой поток, который является свободным, может заняться этой задачей.
Асинхронность в ASP.NET
Асинхронность в ASP.NET может стать большим стимулом для повышения производительности вашего приложения. Вот, как IIS обрабатывает запрос:
Когда запрос получен IIS, он берет поток из пула потоков CLR (IIS не имеет какого-либо пула потоков, а сам вместо этого использует пул потоков CLR) и назначает его ему, который далее обрабатывает запрос. Поскольку количество потоков ограничено, и новые могут быть созданы с определенным пределом, тогда если поток будет находится большую часть времени в состоянии ожидания, то это сильно ударит по вашему серверу, вы можете предположить, что это реальность. Но если вы пишете асинхронный код (который теперь становится очень простым и может быть написан почти аналогично синхронному при использовании новых ключевых слов async / await), то он будет работать намного быстрее, и пропускная способность вашего сервера значительно возрастет, потому что вместо ожидания какого-нибудь завершения, он будет доступен пулу потоков, для нового запроса. Если приложение имеет множество зависимостей и длительный процесс выполнения, то для этого приложения асинхронное программирование будет не меньшем благом.
Итак, теперь мы поняли разницу многопоточного, асинхронного программирования и преимущества, которые мы можем получить, используя асинхронную модель программирования.
Параллелизм, многопоточность, асинхронность
Многие начинающие специалисты путают многопоточное, асинхронное и параллельное программирование. На первый взгляд, может показаться, что это одно и то же — но нет. Тимур Гайфулин, руководитель группы разработки digital-интегратора DD Planet, совместно с Алексеем Гришиным, ведущим разработчиком DD Planet, попробовал разобраться, сколько программных моделей используют C#-разработчики и в чём их отличия. Статью опубликовал сайт tproger.ru.
Существует несколько концепций: синхронное/асинхронное программирование и однопоточные/многопоточные приложения. Причём первая программная модель может работать в однопоточной или многопоточной среде. То есть приложение может быть: синхронным однопоточным, синхронным многопоточным и асинхронным многопоточным.
Отдельной концепцией считается параллелизм, который является подмножеством многопоточного типа приложений. Рассмотрим особенности каждой программной модели подробнее.
Синхронная модель
Потоку назначается одна задача, и начинается её выполнение. Заняться следующей задачей можно только тогда, когда завершится выполнение первой. Эта модель не предполагает приостановку одной задачи, чтобы выполнить другую.
Однопоточность
Система в одном потоке работает со всеми задачами, выполняя их поочерёдно.
Многопоточность
В этом случае речь о нескольких потоках, в которых выполнение задач идет одновременно и независимо друг от друга.
Пример такого концепта — одновременная разработка веб- и мобильного приложений и серверной части, при условии соблюдения архитектурных «контрактов».
Использование нескольких потоков выполнения — один из способов обеспечить возможность реагирования приложения на действия пользователя при одновременном использовании процессора для выполнения задач между появлением или даже во время появления событий пользователя.
Асинхронность
- обрабатывает больше запросов сервера, предоставляя потокам возможность обрабатывать больше запросов во время ожидания результата от запросов ввода-вывода;
- делает пользовательский интерфейс быстрым, выделяя потоки для обработки действий в пользовательском интерфейсе во время ожидания запросов ввода-вывода, передавая затратные по времени операции другим ядрам ЦП.
Если у системы много потоков, то их асинхронная работа выглядит примерно так:
Конструкция async/await
Для работы с асинхронными вызовами в C# необходимы два ключевых слова:
- async — используется в заголовке метода;
- await — вызывающий метод содержит одно или несколько таких выражений.
Они используются вместе для создания асинхронного метода. У асинхронных методов могут быть следующие типы возвращаемых значений:
- Task для асинхронного метода, который выполняет операцию, но не возвращает значение;
- Task для асинхронного метода, возвращающего значение;
- void для обработчика событий;
- начиная с версии 7.0 в языке C# поддерживаются любые типы с доступным методом GetAwaiter;
- начиная с версии 8.0 в языке C# поддерживается интерфейс IAsyncEnumerable для асинхронного метода, который возвращает асинхронный поток.
Сама конструкция 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 Thread.Sleep(8000); Console.WriteLine($"Факториал равен "); > // определение асинхронного метода 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($"Квадрат числа равен "); Console.Read(); > > >
Этот пример приведён лишь для наглядности, особого смысла делать логику вычисления факториала асинхронной нет. Опять же, для имитации долгой работы мы использовали задержку на 8 секунд с помощью методы Thread.Sleep(). Цель была показать: асинхронная задача, которая может выполняться долгое время, не блокирует основной поток — в этом случае метод Main(), и мы можем вводить и обрабатывать данные, продолжая работу с ним.
Параллелизм
Эта программная модель подразумевает, что задача разбивается на несколько независимых подзадач, которые можно выполнить параллельно, а затем объединить результаты. Примером такой задачи может быть Parallel LINQ:
IEnumerable yourData = GetYourData(); var result = yourData.AsParallel() // начинаем обрабатывать параллельно .Select(d => d.CalcAmount()) // Вычисляем параллельно .Where(amount => amount > 0) .ToArray(); // Возвращаемся к синхронной модели
Еще один пример — вычисление среднего значения двумерного массива, когда каждый отдельный поток может подсчитать сумму своей строки, а потом объединить результат и вычислить среднее.
Однако не стоит забывать, что не все задачи поддаются распараллеливанию. Например, описанная выше задача по вычислению факториала, в которой на каждом последующем этапе нужен результат предыдущего.
Какую программную модель выбрать?
Перечисленные программные модели должны применяться в зависимости от задач. Их можно использовать как отдельно во всём приложении, так и сочетать между собой. Главное, чтобы приложение было максимально эффективным и удовлетворяло требования пользователя.
Если речь идет о сложных многопользовательских приложениях, то стремиться стоит к использованию асинхронной модели, так как важна интерактивность и отзывчивость интерфейса. Взаимодействие с пользователем в активном режиме всегда должно быть максимально эффективным, даже если в фоновом режиме в то же время выполняются другие задачи. Издержки асинхронности, например, на переключение исполняемого контекста, в таком случае нивелируются за счет общей эффективности приложения.
В разработке простых приложений, к примеру, парсера документа, необходимости в асинхронности, или даже многопоточности, может и не быть.