Многопоточность vs асинхронность?
Доброго времени суток. Хотелось бы узнать разницу между этими подходами. Разве асинхронное программирование не подразумевает из себя похожее на многопоточность? Когда задачи выполняются в одном потоке?
- Вопрос задан более трёх лет назад
- 6371 просмотр
Комментировать
Решения вопроса 0
Ответы на вопрос 2
Многопоточность vs асинхронность
Ресторан. Клиент заказал яичницу и гренки.
1. Синхронное выполнение: даем задание 1 повару. Он сначала делает яичницу. После того как она готова он приступает к гренкам.
2. Асинхронное выполнение (многопоточность): даем задание 2 поварам. Они приступают условно одновременно. Один делает яичницу, второй гренки и оба конфликтуют из-за доступа к общим ресурсам (перцу, соли, сковородке).
3. Асинхронное выполнение (1 поток): даем задание 1 повару. Он ставит на плиту яичницу и ставит таймер. Ставит гренки и тоже ставит таймер. Пока таймеры не сработали, чтобы не терять время, прибирает кухню. После того как сработают оба таймера — отдает заказ.
Ответ написан более двух лет назад
Комментировать
Нравится 5 Комментировать
Различия в подходах работы соответствующих методов, чья работа должна выполняться долго и чего то ждать (например сетевое взаимодействие но не обязательно само собой).
Синхронное выполнение — это значит все ваши методы, выполняющие какую то задачу работают до тех пор пока эта задача не завершится, например отправка данных в сеть, запись на диск (с оговорками, существует режим записи файлов, при котором запись данных в файл происходит в буфер в памяти, а реальная запись происходит параллельно потом). В этом способе невозможно запустить два одновременных метода, например запись на диск и чтение из сети — что очень неэффективно расходует ресурсы, так как пока тут идет запись на диск — сеть не нагружена и наоборот.
Асинхронное выполнение — это значит вызов начала действия завершается в тот же миг как был вызван, запуская процесс загрузки каким-либо способом (зачастую в другом потоке но не обязательно) и дает вам какие то инструменты получения информации о том, завершилась ли работа или нет, например callback методы, которые вызываются по завершению работы или ошибки, в параметрах которых будет выдан результат (либо для его получения будут другие методы). Т.е. вы можете запустить несколько асинхронных действий одновременно, эффективно расходуя ресурсы.
Многопоточное — в разрезе выше описанной задачи, это подход запуска одновременно нескольких синхронных методов, которые готовы к такому запуску (эта оговорка требует примечаний километра на два текста) в отдельных потоках (треды и/или процессы) — специальных сущностях операционной системы, работающих независимо и одновременно (точнее псевдоодновременно, к примеру у вас 16 ядер процессора а у вас 100 одновременно работающих потоков, чтобы они работали одновременно система сама периодически перекидывает активные потоки между ядрами на короткое время — миллисекунды, замораживая остальные, создавая иллюзию одновременной работы), таким образом ваши методы работают одновременно, но приходится тратить силы на синхронизацию.
Параллелизм, многопоточность, асинхронность
Многие начинающие специалисты путают многопоточное, асинхронное и параллельное программирование. На первый взгляд, может показаться, что это одно и то же — но нет. Тимур Гайфулин, руководитель группы разработки 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(); // Возвращаемся к синхронной модели
Еще один пример — вычисление среднего значения двумерного массива, когда каждый отдельный поток может подсчитать сумму своей строки, а потом объединить результат и вычислить среднее.
Однако не стоит забывать, что не все задачи поддаются распараллеливанию. Например, описанная выше задача по вычислению факториала, в которой на каждом последующем этапе нужен результат предыдущего.
Какую программную модель выбрать?
Перечисленные программные модели должны применяться в зависимости от задач. Их можно использовать как отдельно во всём приложении, так и сочетать между собой. Главное, чтобы приложение было максимально эффективным и удовлетворяло требования пользователя.
Если речь идет о сложных многопользовательских приложениях, то стремиться стоит к использованию асинхронной модели, так как важна интерактивность и отзывчивость интерфейса. Взаимодействие с пользователем в активном режиме всегда должно быть максимально эффективным, даже если в фоновом режиме в то же время выполняются другие задачи. Издержки асинхронности, например, на переключение исполняемого контекста, в таком случае нивелируются за счет общей эффективности приложения.
В разработке простых приложений, к примеру, парсера документа, необходимости в асинхронности, или даже многопоточности, может и не быть.
Параллелизм против многопоточности против асинхронного программирования: разъяснение
В последние время, я выступал на мероприятиях и отвечал на вопрос аудитории между моими выступлениями о Асинхронном программировании, я обнаружил что некоторые люди путали многопоточное и асинхронное программирование, а некоторые говорили, что это одно и тоже. Итак, я решил разъяснить эти термины и добавить еще одно понятие Параллелизм. Здесь есть две концепции и обе они совершенно разные, первая синхронное и асинхронное программирование и вторая – однопоточные и многопоточные приложения. Каждая программная модель (синхронная или асинхронная) может работать в однопоточной и многопоточной среде. Давайте обсудим их подробно.
Синхронная программная модель – это программная модель, когда потоку назначается одна задача и начинается выполнение. Когда завершено выполнение задачи тогда появляется возможность заняться другой задачей. В этой модели невозможно останавливать выполнение задачи чтобы в промежутке выполнить другую задачу. Давайте обсудим как эта модель работает в одно и многопоточном сценарии.
Однопоточность – если мы имеем несколько задач, которые надлежит выполнить, и текущая система предоставляет один поток, который может работать со всеми задачами, то он берет поочередно одну за другой и процесс выглядит так:
Здесь мы видим, что мы имеем поток (Поток 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), то он будет работать намного быстрее, и пропускная способность вашего сервера значительно возрастет, потому что вместо ожидания какого-нибудь завершения, он будет доступен пулу потоков, для нового запроса. Если приложение имеет множество зависимостей и длительный процесс выполнения, то для этого приложения асинхронное программирование будет не меньшем благом.
Итак, теперь мы поняли разницу многопоточного, асинхронного программирования и преимущества, которые мы можем получить, используя асинхронную модель программирования.
- асинхронное программирование
- многопоточное программирование
- параллелизм
- потоки
- задачи
- Высокая производительность
- .NET
- ASP
Вступление
В этом разделе я попытаюсь сформулировать цель своей статьи (можете пропустить его, если хотите просто узнать, как настроить асинхронную многопоточность в Python). Мне потребовалось много времени, чтобы методом проб и ошибок освоить и научиться применять параллельный код. В этом мне помогли StackOverflow, TDS и т. д., а также отличные руководства для изучения основ параллельного программирования.
И все же должен признать: едва вы приступите к распараллеливанию, как перед вами встанет множество проблем при вводе массы данных и осуществлении различных операций. К примеру, проблема, над которой я работал, требовала 15 миллионов вызовов API, которые сопровождались проблемами масштабируемости. Я не эксперт в параллельном программировании, просто хотел бы поделиться тем, что сам узнал, и теми сложностями, с которыми столкнулся. Надеюсь, мое руководство будет полезным для других новичков, таких как я!
Асинхронность против многопоточности
На первых порах я путался в этих понятиях, поэтому, думаю, не лишним будет на них остановиться. Асинхронное программирование обычно используется в случаях, когда определенное действие выполняется медленно, блокируя выполнение следующих. Вы часто видите это при вводе-выводе, например при сетевых вызовах, когда возврат одного из них может занять сотни миллисекунд.
Когда программа ждет ответа на вызов, вы бездействуете и тратите драгоценное время. Выполняя вызовы асинхронно, вы можете немедленно отправлять их один за другим и перехватывать позже с помощью функции обратного вызова. Это что-то вроде питчера, способного бросить сразу несколько мячей вместо одного.
С другой стороны, многопоточность приводит к появлению большего количества потоков, каждый из которых может выполнять действия параллельно. Поэтому, если асинхронное программирование — это питчер с несколькими мячами, то многопоточность — игра с целой командой питчеров. Здесь можно найти более подробное объяснение.
Как быстро это происходит?
Очень быстро. При решении моей задачи каждый вызов API занимал около 100 миллисекунд. При последовательном выполнении я мог получать около 10 вызовов в секунду. Чтобы получить 15 миллионов вызовов, мне пришлось бы потратить примерно 3 недели.
С помощью параллельного программирования я смог выполнить более 2500 вызовов в секунду, получив все 15 миллионов всего за 2 часа (такого ускорения я добился, запустив модуль Kubernetes, но ваши результаты могут немного отличаться в зависимости от типа машины, лимита памяти и возможностей процессора).
Настройка
Допустим, у нас есть медленная функция, на выполнение которой требуется 100 миллисекунд.
def slow_function(data):
time.sleep(100/1000)
return data
Мы хотим запустить эту функцию много раз и экспортировать результат:
if __name__ == "__main__":
output = []
for i in range(10000):
output.append(slow_function(i)) write_to_file(output)
10000 последовательных вызовов займут не менее 1000 секунд (100 м/с на вызов для 10000 вызовов) или около 16 минут.
Распараллеливание
Для распараллеливания мы можем использовать встроенную в Python библиотеку multiprocessing :
import multiprocessing as mp
Сначала мы должны написать функцию обратного вызова, которая сообщит программе, что делать, как только функция slow_function выполнит возврат. Она принимает переменную result , которая является результатом функции slow_function , и производит с ней определенные действия. В этом случае мы просто добавим ее в список:
def catch(result):
global output
output.append(result)
Наконец, мы настраиваем пул потоков и вызываем функцию асинхронно:
JOBS = 200 # 1if __name__ == "__main__":
mp.set_start_method("spawn") # 2
pool = mp.Pool(JOBS) # 3output = []
for i in range(10000):
pool.apply_async(slow_function, args=(i, ), \
callback=catch) # 4 pool.close() # 5
pool.join() write_to_file(output)
- Переменная JOBS определяет, сколько потоков вы хотите создать. Чем больше вы порождаете их, тем быстрее будет выполняться программа. Однако слишком большое количество может создать излишнюю нагрузку на процессор или переполнение памяти. Поэкспериментируйте, пока не найдете оптимальный вариант (я пробовал от 4 до 200 с разным количеством, подходящим для разных машин).
- Установите для метода запуска значение “порождение”, чтобы избежать этого проблемного момента.
- Настройте пул потоков с выбранным количеством потоков.
- Вызовите функцию slow_function с помощью pool.apply_async() при передаче функции обратного вызова.
- Используйте pool.close() , чтобы запретить потокам принимать новую работу, и pool.join() , чтобы завершить потоки (более подробную информацию см. на этом форуме).
**Примечание: из-за параллельного характера программы выходные данные не будут упорядочены. В этом есть свой плюс: вы можете возвращать входные данные с каждым выводом, чтобы сопоставлять их по мере необходимости.
Если вы запустите этот код, он будет хорошо работать для входных данных среднего размера (~10–100 тыс. действий). Однако, если входные данные намного больше, что потребует миллионы операций, то вы можете столкнуться с двумя основными проблемами:
- из-за значительного ускорения работы может произойти перегрузка процессора;
- выходные данные могут быть настолько большими, что переполнят память.
Решение для процессора
Чтобы устранить проблему с процессором, я запускал все по частям, с небольшими интервалами между каждым блоком:
JOBS = 200
CHUNKSIZE = 1000def process_chunk(chunk, pool):
for data in chunk:
pool.apply_async(slow_function, args=(data, ), \
callback=catch)if __name__ == "__main__":
mp.set_start_method("spawn")
pool = mp.Pool(JOBS) output = []
chunk = []
for i in range(10000):
chunk.append(i) if (i+1) % CHUNKSIZE == 0:
process_chunk(chunk, pool)
chunk = []
time.sleep(500/1000) pool.close()
pool.join() write_to_file(output)
Вы также можете уменьшить количество заданий, чтобы уменьшить нагрузку на процессор.
Решение для памяти
Чтобы решить проблему с памятью, я экспортировал выходные данные пакетами (каждые несколько блоков) и очистил хранилище данных в памяти (а также делал более длительные интервалы между пакетами):
JOBS = 200
CHUNKSIZE = 1000
BATCHSIZE = 5def process_chunk(chunk, pool):
for data in chunk:
pool.apply_async(slow_function, args=(data, ), \
callback=catch)if __name__ == "__main__":
mp.set_start_method("spawn")
pool = mp.Pool(JOBS) output = []
chunk = []
for i in range(10000):
chunk.append(i) if (i+1) % CHUNKSIZE == 0:
process_chunk(chunk, pool)
chunk = []
time.sleep(500/1000) if (i+1) % (BATCHSIZE * CHUNKSIZE) == 0:
write_to_file(output)
output = [] pool.close()
pool.join()
Полный код
Вот полный распараллеленный код, включающий в себя решения проблем с процессором и памятью.
import time
import timeit
import multiprocessing as mp# установите параметры тут
JOBS = 200
CHUNKSIZE = 1000
BATCHSIZE = 5def slow_function(data):
time.sleep(100/1000)
return datadef catch(result):
global output
output.append(result)def process_chunk(chunk, pool):
for data in chunk:
pool.apply_async(slow_function, args=(data, ), \
callback=catch)if __name__ == "__main__":
mp.set_start_method("spawn")
pool = mp.Pool(JOBS) start = timeit.default_timer() output = []
chunk = []
for i in range(10000):
chunk.append(i) if (i+1) % CHUNKSIZE == 0:
process_chunk(chunk, pool)
chunk = []
time.sleep(500/1000) if (i+1) % (BATCHSIZE * CHUNKSIZE) == 0:
write_to_file(output)
output = [] pool.close()
pool.join() stop = timeit.default_timer()
print("##### done in ", stop - start, " seconds #####")
Я настроил указанные выше параметры как глобальные, в том числе количество потоков, размер блока и объем пакета. Поэкспериментируйте с ними, чтобы получить оптимальную производительность для своей машины и входных данных.
Моя производительность достигла порядка 10 000 000+ вызовов. При более высоких значениях может возникнуть больше проблем и трудностей, требующих поиска решений.
Контрольное время
Ранее я подсчитал, что для последовательного запуска этого примера программы потребуется не менее 1000 секунд. При использовании приведенного выше кода, работающего локально на стандартном 8-ядерном Macbook Pro 2019 года, я оценил эффективность работы в ~11 секунд, что примерно в 100 раз быстрее.
- Отладка кода на Python с помощью icecream
- 9 важных сниппетов Python для оптимизации работы со скриптами
- 3 способа локального хранения и чтения учетных данных в Python