Чем асинхронность отличается от многопоточности
Перейти к содержимому

Чем асинхронность отличается от многопоточности

  • автор:

В чём отличие многопоточности и асинхронности в контексте .NET C#?

Добрый день.
Я не до конца понимаю в чём ключевое отличие асинхронности и многопоточности.
Как я понимаю многопоточность обеспечивается классом Thread(инкапсулированный поток ОС), а асинхронность — Task, Async, Await, ThreadPool. И мол вот, при использовании этих конструкций мы не блокируем вызывающий поток. Но ведь при классической многопоточности никакой поток так же не блокируется, мы просто его создаем, кормим ему задачу и всё — основной поток свободен. Тем более при асинхронности практически всегда подразумевается многопоточность — тот же ThreadPool который работает с тасками и запускает задачи в заранее заготовленных фоновых потоках. То же самое и с async/await. Можете пожалуйста помочь упорядочить всю эту информацию, заранее спасибо!

  • Вопрос задан 10 июн. 2023
  • 1438 просмотров

3 комментария

Простой 3 комментария

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

Когда у нас много ввода-вывода и мало процессорной работы, то асинхронность может быть очень эффективна, причём за счёт синтаксических конструкций (async/await) можно писать код очень удобно, наглядно и эффективно. Но любая вычислительная задача, любой синхронный код могут привести к заметным задержкам на выполнение других задач, это недостаток.

При этом асинхронная функция запускается быстро и дёшево, а запуск потока — это более сложная операция. Поэтому много коротких асинхронных функций это нормально, а много потоков с коротким временем жизни — нет. При многопоточности обычно или делят задачу на большие части, выполняющиеся в разных потоках, или разделяют по функционалу (например, GUI в одном потоке, вычислительные задачи в другом).

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

Griboks

Griboks @Griboks Куратор тега C#

Разница только в планировщике: для потоков используется системный планировщик, для корутин — внутренний планировщик виртуальной машины в текущем потоке.

p. s.
Но это не точно. Надо бы почитать в оф. документации.

Чем отличаются Конкурентность, Многопоточность, Асинхронность и Параллелилизм?

В чем отличия?
Почему есть классы Async и Thread? А нет Conc и Parallel? Зачем придумали эти термины если они означают только подход к разделению ресурсов? Тоесть на 1 CPU многопоточность или async это Конкурентность. На 100500 CPU это уже Параллелилизм.

#1
13:19, 2 июня 2018

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

  • Sbtrn. Devil
  • Постоялец

#2
16:10, 2 июня 2018

Это всё одно и то же яйцо с различных ракурсов: физического, системного, логического и алгоритмического соответственно.

#3
16:27, 2 июня 2018

Sbtrn. Devil
окей, броу. Но на собеседованиях такой ответ не примут. Типа «ол ве сейм, бал лоджикали дифеерент»? И сразу рожи вкреривь и протяжное океееееей. Конкретно, плиз.

#4
16:31, 2 июня 2018

lookid
> Конкретно, плиз.
Начните с книжки Рихтера.

  • Имбирная Ведьмочка
  • Участник

#5
18:00, 2 июня 2018

Асинхронность — это когда выполнение одной задачи начинается прежде, чем закончится выполнение другой. Например, ты можешь отправить запрос на чтение из файла, пойти отрендерить пару фреймов, и затем вернуться за готовым результатом в буфере — это асинхронный I/O.
Параллелизм — это когда в один момент времени выполняется сразу несколько задач. Когда у тебя в одном контексте работает больше одного физического ядра — будь то в одном процессоре, или соединённые по сети — это настоящий параллелизм. На одном ядре некоторые реализации заставляют симулировать за счёт мультиплексии во времени — это фейк и гей. Настоящий параллелизм выделяется тем, что в нём сложнее синхронизация — там надо и кэши учитывать, и шину как-то делить, и даже отключение прерываний больше не спасает от дата рейсов.
Многопоточность — это абстракция процессоров со стороны ОС, подобно тому, как процессы — это абстракция памяти. Когда ты запускаешь программу — ей не выдаётся своя физическая планка памяти, вместо этого для ней размещается собственный процесс. Когда тебе нужна вытесняющая асинхронность — тебе не выдаётся отдельное ядро в личное распоряжение, вместо этого ОС даёт тебе поток.
Параллелизм — это один из способов реализации многопоточности. Многопоточность можно реализовать и без настоящего параллелизма, если время от времени насильно переключать один процессор между потоками.
Многопоточность — это один из способов реализации асинхронности. Асинхронность можно реализовать и в одном потоке, если вместо блокирующих вызовов использовать схему заказал операцию — покурил — забрал результат.
Конкурентность — это когда на один ресурс претендует больше пользователей, чем допускается этим ресурсом.
В контексте асинхронности, ресурс — это данные, использование — это чтения и записи, а ограничение — в течение обновления одной единицы данных к объекту может обращаться только код-обновитель, а остальным нельзя, ни читать, ни писать в те же данные. Например — ты делаешь заказ «запиши вот эту структуру в файл», уходишь заниматься другими делами, и в ходе этих дел случайно меняешь эту структуру — происходит обновление, хотя в это же время дисковый контроллер считывает данные для записи в накопитель. В итоге — в файле окажется записан монстр из перемешанных значений, хотя программа выполняется только в один поток на одном процессоре.

#6
18:02, 2 июня 2018

Delfigamer
Кончайте дурить народ.

  • Имбирная Ведьмочка
  • Участник

#7
18:11, 2 июня 2018

gudleifr
Окей.

concurrent
adjective
1. Existing, happening, or done at the same time.
‘there are three concurrent art fairs around the city’

thread
noun
2.2. Computing A programming structure or process formed by linking a number of separate elements or subroutines, especially each of the tasks executed concurrently in multithreading.

multi-
combining form
More than one; many.
‘multicolour’
‘multicultural’

asynchronous
adjective
2. Computing Telecommunications Controlling the timing of operations by the use of pulses sent when the previous operation is completed rather than at regular intervals.

parallel
adjective
4. Computing Involving the simultaneous performance of operations.

#8
18:21, 2 июня 2018

Delfigamer
А теперь возьмите Рихтера и перестаньте изобретать велосипеды.
Если же хотите «великой истины», то см. «Взаимодействие последовательных процессов» Дейкстры и тех ребят из XEROX — ТЕМА #39, АБЗАЦ #764.

#9
18:46, 2 июня 2018

> теперь возьмите Рихтера
Ты про Джеффри Рихтера? Какую конкретно книгу?

#10
18:49, 2 июня 2018

Ghost2
> Ты про Джеффри Рихтера?
Да?

Ghost2
> Какую конкретно книгу?
Windows для профессионалов.

#11
20:17, 2 июня 2018

Delfigamer
> (заметьте, что это нихрена не «конкурентный»)
ну это в русском языке «конкурент» это в первую очеред соперник.
почему-то перевод «одновременность» у нас даже не пытались привить.

Интересно, что concurrency, может привести к race-conditions. И тогда смысл «конкурент» станет весьма актуальным 😀

> Асинхронность
на самом деле не подразумевает многопоточность или параллелилизм.
это просто указывает что действие будет выполнено не сразу после заявления.
(например: неблокирующие сокеты. Реальная реализация оставлена за кадром. Единственное что API обещает, так это «принять» данные, а статус проверьте потом. Асинхронность исполнения)

> Многопоточность
Multithreading.
(в начале 2000х гулял термин «Нить», а не поток. Но вот «многониточное» приложение слышать не пришлось)
Когда говорят о много поточности, то имеют в виду Потоки процесса. Т.е. некую объект/сущность системы, которая(ые) выполняется в рамках одного процесса.
Важно отличать о «многопроцессности».

> Конкурентность, и Параллелилизм
любым методом выполняющиеся одновременно задачи/процессы (и многопоточность и/или многозадачность)

#12
20:21, 2 июня 2018

lookid
> Но на собеседованиях такой ответ не примут.

а ты куда хочешь собеседоваться ? могу опытом поделиться 🙂

skalogryz
> (в начале 2000х гулял термин «Нить», а не поток

эх, помню как году в 94 прочитал про нити threads

#13
20:26, 2 июня 2018

skalogryz
Тут дело в том, что если ядер меньше, чем задач-потоков, что это — конкюренси. Если число ядер совпадает с числом задач-потоков — параллелизм. Асинк хранит результат и его можно загетать-заколбечить. По-факту, потоки уже не нужны. Но вот дело в том, что асинк поражает поток или нет? То какая разница тогда.

#14
20:30, 2 июня 2018

innuendo
Сервис на 35 000 000 человек на С#. Бекенд к 10 сервисов. Skype Core Team.

Вступление

В этом разделе я попытаюсь сформулировать цель своей статьи (можете пропустить его, если хотите просто узнать, как настроить асинхронную многопоточность в 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) # 3
output = []
for i in range(10000):
pool.apply_async(slow_function, args=(i, ), \
callback=catch) # 4
pool.close() # 5
pool.join()
write_to_file(output)
  1. Переменная JOBS определяет, сколько потоков вы хотите создать. Чем больше вы порождаете их, тем быстрее будет выполняться программа. Однако слишком большое количество может создать излишнюю нагрузку на процессор или переполнение памяти. Поэкспериментируйте, пока не найдете оптимальный вариант (я пробовал от 4 до 200 с разным количеством, подходящим для разных машин).
  2. Установите для метода запуска значение “порождение”, чтобы избежать этого проблемного момента.
  3. Настройте пул потоков с выбранным количеством потоков.
  4. Вызовите функцию slow_function с помощью pool.apply_async() при передаче функции обратного вызова.
  5. Используйте pool.close() , чтобы запретить потокам принимать новую работу, и pool.join() , чтобы завершить потоки (более подробную информацию см. на этом форуме).

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

Если вы запустите этот код, он будет хорошо работать для входных данных среднего размера (~10–100 тыс. действий). Однако, если входные данные намного больше, что потребует миллионы операций, то вы можете столкнуться с двумя основными проблемами:

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

Решение для процессора

Чтобы устранить проблему с процессором, я запускал все по частям, с небольшими интервалами между каждым блоком:

JOBS = 200
CHUNKSIZE = 1000
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)
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 = 5
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)
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 = 5
def slow_function(data):
time.sleep(100/1000)
return data
def 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

Параллелизм против многопоточности против асинхронного программирования: разъяснение

В последние время, я выступал на мероприятиях и отвечал на вопрос аудитории между моими выступлениями о Асинхронном программировании, я обнаружил что некоторые люди путали многопоточное и асинхронное программирование, а некоторые говорили, что это одно и тоже. Итак, я решил разъяснить эти термины и добавить еще одно понятие Параллелизм. Здесь есть две концепции и обе они совершенно разные, первая синхронное и асинхронное программирование и вторая – однопоточные и многопоточные приложения. Каждая программная модель (синхронная или асинхронная) может работать в однопоточной и многопоточной среде. Давайте обсудим их подробно.

Синхронная программная модель – это программная модель, когда потоку назначается одна задача и начинается выполнение. Когда завершено выполнение задачи тогда появляется возможность заняться другой задачей. В этой модели невозможно останавливать выполнение задачи чтобы в промежутке выполнить другую задачу. Давайте обсудим как эта модель работает в одно и многопоточном сценарии.

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

image

Здесь мы видим, что мы имеем поток (Поток 1) и 4 задачи, которые необходимо выполнить. Поток начинает выполнять поочередно одну за одной и выполняет их все. (Порядок, в котором задачи выполняются не влияет на общее выполнение, у нас может быть другой алгоритм, который может определять приоритеты задач.

Многопоточность – в этом сценарии, мы использовали много потоков, которые могут брать задачи и приступать к работе с ними. У нас есть пулы потоков (новые потоки также создаются, основываясь на потребности и доступности ресурсов) и множество задач. Итак, поток может работать вот так:

image

Здесь мы можем видеть, что у нас есть 4 потока и столько же задач для выполнения, и каждый поток начинает работать с ними. Это идеальный сценарий, но в обычных условиях мы используем большее количество задач чем количество доступных потоков, таким образом освободившийся поток получает другое задание. Как уже говорилось создание нового потока не происходит каждый раз потому что для этого требуются системные ресурсы такие как процессор, память и начальное количество потоков должно быть определенным.

Теперь давайте поговорим о Асинхронной модели и как она ведет себя в одно и многопоточной среде.

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

image

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

Если наша система способно иметь много потоков тогда все потоки могут работать в асинхронной модели как показано ниже:

image

Здесь мы можем видеть, что одна и та же задача скажем Т4, Т5, Т6 … обрабатывается несколькими потоками. Это красота этого сценария. Как мы можем видеть, что задача Т4 начала выполнение первой Потоком 1 и завершен Потоком 2. Подобным образом задча Т6 выполнена Потоком 2, Потоком 3 и Потоком 4. Это демонстрирует максимальное использование потоков.

Итак, до сих пор мы обсудили 4 сценария:

  • Синхронный однопоточный
  • Синхронный многопоточный
  • Асинхронный однопоточный
  • Асинхронный многопоточный

Параллелизм

Проще говоря параллелизм способ обработки множественных запросом одновременно. Так как мы обсуждали два сценария когда обрабатывались множественные запросы, многопоточное программирование и асинхронная модель (одно и многопоточная). В случае асинхронной модели будь она однопоточной или многопоточной, в то время, когда выполняются множество задач, некоторые из них приостанавливаются, а некоторые выполняются. Существует много особенностей, но это выходит за рамки этой публикации.

Как обсуждалось ранее новая эпоха за асинхронным программированием. Почему это так важно?

Преимущества асинхронного программирования

Существует две вещи очень важные для каждого приложения – удобство использования и производительность. Удобство использования потому что пользователь нажав кнопку чтобы сохранить некоторые данные что в свою очередь требует выполнения множества задач таких как чтение и заполнение данных во внутреннем объекте, установление соединения с SQL и сохранения его там. В свою очередь SQL запускается на другой машине в сети и работает под другим процессом, это может потребовать много время. Таким образом если запрос обрабатывается одним процессом экран будет находится в зависшем состоянии до тех пор, пока процесс не завершится. Вот почему сегодня многие приложения и фреймворки полностью полагаются на асинхронную модель.

Производительность приложения и системы также очень важны. Было замечено в то время как выполняется запрос, около 70-80% из них попадают в ожидании зависимых задач. Таким образом, это может быть максимально использовано в асинхронном программирование, где, как только задача передается другому потоку (например, SQL), текущий поток сохраняет состояние и доступен для выполнения другого процесса, а когда задача sql завершается, любой поток, который является свободным, может заняться этой задачей.

Асинхронность в ASP.NET

Асинхронность в ASP.NET может стать большим стимулом для повышения производительности вашего приложения. Вот, как IIS обрабатывает запрос:

image

Когда запрос получен IIS, он берет поток из пула потоков CLR (IIS не имеет какого-либо пула потоков, а сам вместо этого использует пул потоков CLR) и назначает его ему, который далее обрабатывает запрос. Поскольку количество потоков ограничено, и новые могут быть созданы с определенным пределом, тогда если поток будет находится большую часть времени в состоянии ожидания, то это сильно ударит по вашему серверу, вы можете предположить, что это реальность. Но если вы пишете асинхронный код (который теперь становится очень простым и может быть написан почти аналогично синхронному при использовании новых ключевых слов async / await), то он будет работать намного быстрее, и пропускная способность вашего сервера значительно возрастет, потому что вместо ожидания какого-нибудь завершения, он будет доступен пулу потоков, для нового запроса. Если приложение имеет множество зависимостей и длительный процесс выполнения, то для этого приложения асинхронное программирование будет не меньшем благом.

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

  • асинхронное программирование
  • многопоточное программирование
  • параллелизм
  • потоки
  • задачи
  • Высокая производительность
  • .NET
  • ASP

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *