Снова и снова, я вижу, это говорит, что использование async
- await
не создает никаких дополнительных потоков. Это не имеет смысла, потому что единственный способ, которым компьютер может делать больше, чем 1 вещь одновременно
- На самом деле делать больше, чем 1 вещь одновременно (выполнение параллельно, используя несколько процессоров)
- Имитация этого путем планирования задач и переключения между ними (немного A, немного B, немного A и т. Д.)
Так что, если async
- await
ни один из них, то как это может сделать приложение отзывчивым? Если существует только 1 поток, то вызов любого метода означает ожидание завершения метода, прежде чем делать что-либо еще, и методы внутри этого метода должны ждать результата, прежде чем продолжить, и так далее.
c#
.net
multithreading
asynchronous
async-await
Мисс корлиб
источник
источник
await
/async
работает без создания каких-либо потоков.Ответы:
На самом деле, async / await не так волшебно. Полная тема довольно обширна, но я думаю, что для быстрого, но достаточно полного ответа на ваш вопрос мы справимся.
Давайте рассмотрим простое событие нажатия кнопки в приложении Windows Forms:
Я собираюсь явно не говорить о том, что он
GetSomethingAsync
сейчас возвращает. Скажем так, это то, что завершится, скажем, через 2 секунды.В традиционном, не асинхронном мире ваш обработчик событий нажатия кнопки будет выглядеть примерно так:
Когда вы нажмете кнопку в форме, приложение будет зависать примерно на 2 секунды, пока мы ждем завершения этого метода. То, что происходит, - то, что "насос сообщений", в основном петля, заблокирован.
Этот цикл непрерывно спрашивает окна: «Кто-нибудь что-то сделал, например, переместил мышь, что-то щелкнул? Нужно ли что-то перекрашивать? Если так, скажите мне!» а затем обрабатывает это «что-то». Этот цикл получил сообщение о том, что пользователь нажал «button1» (или эквивалентный тип сообщения из Windows), и в итоге вызвал наш
button1_Click
метод выше. Пока этот метод не возвращается, этот цикл застрял в ожидании. Это занимает 2 секунды, и в течение этого сообщения не обрабатываются.Большинство вещей, которые имеют дело с окнами, выполняется с помощью сообщений, что означает, что если цикл обработки сообщений прекращает прокачивать сообщения, даже на секунду, он быстро заметен пользователем. Например, если вы переместите блокнот или любую другую программу поверх вашей собственной программы, а затем снова уйдете, в вашу программу будет отправлен поток сообщений рисования, указывающих, какая область окна, которая теперь внезапно снова стала видимой. Если цикл обработки сообщений обрабатывает что-то заблокированное, то рисование не выполняется.
Итак, если в первом примере
async/await
не создаются новые темы, как это происходит?Хорошо, что происходит, что ваш метод разделен на две части. Это один из тех общих вопросов, поэтому я не буду вдаваться в подробности, но достаточно сказать, что метод разбит на две вещи:
await
, включая вызовGetSomethingAsync
await
Иллюстрация:
Переставленные:
В основном метод выполняется так:
await
Он вызывает
GetSomethingAsync
метод, который делает свое дело, и возвращает что-то, что завершит 2 секунды в будущемПока что мы все еще в исходном вызове button1_Click, происходящем в основном потоке, который вызывается из цикла сообщений. Если выполнение кода
await
занимает много времени, пользовательский интерфейс все равно будет зависать. В нашем примере не так многоЧто
await
ключевое слово, вместе с каким - то умным магии компилятора, делает то , что это в основном что - то вроде «Хорошо, вы знаете , что я собираюсь просто вернуться из щелчка кнопки обработчика событий здесь. Если вы (как, вещей мы» "жду завершения), дайте мне знать о завершении, дайте мне знать, потому что у меня еще есть код для выполнения".На самом деле он сообщит классу SynchronizationContext о том, что это сделано, и, в зависимости от текущего контекста синхронизации, который находится в данный момент в игре, будет поставлен в очередь на выполнение. Класс контекста, используемый в программе Windows Forms, поставит его в очередь, используя очередь, которую качает цикл сообщений.
Таким образом, он возвращается обратно к циклу сообщений, который теперь может продолжать качать сообщения, такие как перемещение окна, изменение его размера или нажатие других кнопок.
Для пользователя пользовательский интерфейс теперь снова реагирует, обрабатывая другие нажатия кнопок, изменяя размеры и, самое главное, перерисовывая , чтобы он не зависал.
await
и продолжает выполнение остальной части метода. Обратите внимание, что этот код снова вызывается из цикла сообщений, поэтому, если этот код делает что-то длинное безasync/await
правильного использования , он снова заблокирует цикл сообщений.Есть много движущихся частей под капотом здесь , так что здесь некоторые ссылки на дополнительную информацию, я собирался сказать «вам это нужно», но эта тема является достаточно широким , и это довольно важно знать некоторые из этих движущихся частей . Неизменно вы поймете, что async / await все еще является утечкой. Некоторые из базовых ограничений и проблем все еще проникают в окружающий код, и если они этого не делают, вам обычно приходится отлаживать приложение, которое ломается случайным образом, по-видимому, без веской причины.
Хорошо, а что если
GetSomethingAsync
закрутить поток, который завершится через 2 секунды? Да, тогда очевидно, что есть новая тема в игре. Этот поток, однако, не из- за асинхронности этого метода, а потому, что программист этого метода выбрал поток для реализации асинхронного кода. Почти все асинхронные операции ввода-вывода не используют поток, они используют разные вещи.async/await
сами по себе не раскручивают новые потоки, но, очевидно, «вещи, которые мы ждем», могут быть реализованы с использованием потоков.В .NET есть много вещей, которые не обязательно ускоряют поток сами по себе, но по-прежнему асинхронны:
SomethingSomethingAsync
илиBeginSomething
и,EndSomething
и в этомIAsyncResult
участвует.Обычно эти вещи не используют нить под капотом.
Итак, вы хотите что-то из этого "широкого материала темы"?
Что ж, давайте спросим попробуйте Roslyn о нашем нажатии кнопки:
Попробуйте Рослин
Я не собираюсь ссылаться на полностью сгенерированный класс здесь, но это довольно ужасные вещи.
источник
await
можно использовать так, как вы это описываете, но в целом это не так. Запланированы только обратные вызовы (в пуле потоков) - между обратным вызовом и запросом поток не требуется.Я объясняю это в полной мере в моем блоге Там нет резьбы .
Таким образом, современные системы ввода / вывода интенсивно используют DMA (прямой доступ к памяти). На сетевых картах, видеокартах, контроллерах жестких дисков, последовательных / параллельных портах и т. Д. Имеются специальные выделенные процессоры. Эти процессоры имеют прямой доступ к шине памяти и выполняют чтение / запись независимо от процессора. ЦП просто нужно уведомить устройство о расположении в памяти, содержащей данные, и затем он может делать свое дело, пока устройство не создаст прерывание, уведомляющее ЦП о завершении чтения / записи.
Когда операция выполняется, процессор не выполняет никаких действий, и, следовательно, нет потока.
источник
Task.Run
наиболее подходит для действий , связанных с процессором , но также имеет несколько других применений.Это не то, чего ждут ни те, ни другие . Помните, цель
await
не состоит в том, чтобы сделать синхронный код магически асинхронным . Он позволяет использовать те же методы, которые мы используем для написания синхронного кода при вызове асинхронного кода . Задача Await - сделать код, который использует операции с высокой задержкой, похожим на код, который использует операции с низкой задержкой . Эти операции с высокой задержкой могут выполняться в потоках, они могут выполняться на оборудовании специального назначения, они могут разбивать свою работу на маленькие части и помещать ее в очередь сообщений для последующей обработки потоком пользовательского интерфейса. Они делают что-то для достижения асинхронности, но оните, которые делают это. Await просто позволяет вам воспользоваться этой асинхронностью.Кроме того, я думаю, что вам не хватает третьего варианта. Мы, старики, сегодня дети с их рэп-музыкой должны слезть с моего газона и т. Д. - помните мир Windows в начале 1990-х годов. Не было многопроцессорных машин и планировщиков потоков. Вы хотели запустить два приложения Windows одновременно, вы должны были уступить . Многозадачность была кооперативной . ОС сообщает процессу, что он запускается, и, если он ведет себя плохо, он останавливает обслуживание всех других процессов. Он работает до тех пор, пока не уступит, и каким-то образом он должен знать, как определить, где он остановился, в следующий раз, когда операционная система вернет ему управление., Однопоточный асинхронный код во многом похож на этот, с «await» вместо «yield». Ожидание означает: «Я запомню, где я остановился здесь, и позволю кому-то еще какое-то время бежать; перезвоните мне, когда задача, которую я жду, завершена, и я заберу то, где я остановился». Я думаю, вы можете увидеть, как это делает приложения более отзывчивыми, как это было в Windows 3 дня.
Есть ключ, который вам не хватает. Метод может вернуться до завершения своей работы . В этом суть асинхронности. Метод возвращает, он возвращает задачу, которая означает, что «эта работа выполняется; скажите мне, что делать, когда она будет завершена». Работа метода не выполнена, даже если он вернулся .
Перед оператором ожидания вы должны были написать код, который выглядел как спагетти, пропущенный через швейцарский сыр, чтобы иметь дело с тем фактом, что у нас есть работа после завершения, но с десинхронизацией возврата и завершения . Await позволяет вам писать код, который выглядит как возврат и завершение синхронизации без их фактической синхронизации.
источник
yield
ключевое слово. Иasync
методы, и итераторы в C # являются формой сопрограммы , которая является общим термином для функции, которая знает, как приостановить свою текущую операцию для возобновления позже. У многих языков в эти дни есть сопрограммы или подобные ему потоки управления.Я действительно рад, что кто-то задал этот вопрос, потому что я долгое время считал, что потоки необходимы для параллелизма. Когда я впервые увидел петли событий , я подумал, что это ложь. Я подумал про себя: «Этот код не может быть параллельным, если он выполняется в одном потоке». Имейте в виду, что это после того, как я уже прошел через борьбу понимания различия между параллелизмом и параллелизмом.
После собственных исследований я наконец нашел недостающую часть
select()
. В частности, IO мультиплексирование, реализованы различными ядрами под разными названиями:select()
,poll()
,epoll()
,kqueue()
. Это системные вызовы, которые, хотя детали реализации различаются, позволяют передавать набор дескрипторов файлов для просмотра. Затем вы можете сделать еще один вызов, который блокирует, пока один из отслеживаемых файловых дескрипторов не изменится.Таким образом, можно дождаться набора событий ввода-вывода (основной цикл событий), обработать первое событие, которое завершается, а затем вернуть управление обратно в цикл событий. Промыть и повторить.
Как это работает? Ну, краткий ответ - это магия ядра и аппаратного уровня. Помимо процессора, в компьютере много компонентов, и эти компоненты могут работать параллельно. Ядро может управлять этими устройствами и напрямую связываться с ними для получения определенных сигналов.
Эти системные вызовы мультиплексирования ввода-вывода являются фундаментальным строительным блоком однопоточных циклов событий, таких как node.js или Tornado. Когда вы
await
выполняете функцию, вы наблюдаете за определенным событием (завершение этой функции), а затем возвращаете управление в основной цикл событий. Когда событие, которое вы смотрите, завершается, функция (в конце концов) начинает с того места, где она остановилась. Функции, которые позволяют вам приостанавливать и возобновлять вычисления таким образом, называются сопрограммами .источник
await
иasync
используйте задачи, а не потоки.Фреймворк имеет пул потоков, готовых выполнить некоторую работу в форме объектов Task ; Отправка задачи в пул означает выбор свободной, уже существующей 1-й нити для вызова метода действия задачи.
Создание задачи - это создание нового объекта, намного быстрее, чем создание нового потока.
Поскольку к Задаче можно прикрепить Продолжение , это новый объект Задачи, который должен быть выполнен после завершения потока.
Поскольку они
async/await
используют Задачу, они не создают новую тему.Хотя техника программирования прерываний широко используется в каждой современной ОС, я не думаю, что она здесь уместна.
Вы можете иметь две связанные с процессором задачи, выполняемые параллельно (фактически с чередованием) в одном процессоре
aysnc/await
.Это нельзя объяснить просто тем, что ОС поддерживает организацию очередей IORP .
В прошлый раз, когда я проверял, что компилятор преобразовал
async
методы в DFA , работа делится на этапы, каждый из которых заканчиваетсяawait
инструкцией. Начинает свою задачу и присоединить его продолжение , чтобы выполнить следующий шаг.await
В качестве примера концепции приведу пример псевдокода.
Вещи упрощаются ради ясности и потому, что я не помню точно все детали.
Это превращается в нечто подобное
1 На самом деле пул может иметь свою политику создания задач.
источник
Я не собираюсь конкурировать с Эриком Липпертом или Лассе В. Карлсеном и другими, я просто хотел бы привлечь внимание к еще одному аспекту этого вопроса, который, я думаю, прямо не упоминался.
Использование
await
на нем собственном не делает ваше приложение волшебным образом реагировать. Если что бы вы ни делали в методе, от которого вы ожидаете, из блоков потока пользовательского интерфейса, он все равно будет блокировать ваш пользовательский интерфейс так же, как это было бы с не ожидаемой версией .Вы должны написать свой ожидаемый метод специально, чтобы он либо порождал новый поток, либо использовал что-то вроде порта завершения (который будет возвращать выполнение в текущем потоке и вызывать что-то еще для продолжения всякий раз, когда порт завершения сигнализируется). Но эта часть хорошо объясняется в других ответах.
источник
Вот как я все это вижу, это может быть не очень технически точно, но это помогает мне, по крайней мере :).
Есть в основном два типа обработки (вычисления), которые происходят на машине:
Таким образом, когда мы пишем кусок исходного кода, после компиляции, в зависимости от объекта, который мы используем (и это очень важно), обработка будет привязана к процессору или IO , и фактически она может быть связана с комбинацией обе.
Некоторые примеры:
FileStream
объекта (который является потоком), обработка будет, скажем, 1% CPU и 99% IO.NetworkStream
объекта (который является потоком), обработка будет, скажем, 1% CPU и 99% IO.Memorystream
объекта (который является Stream), обработка будет на 100% привязана к процессору.Итак, как вы видите, с точки зрения объектно-ориентированного программиста, хотя я всегда обращаюсь к
Stream
объекту, то, что происходит ниже, может сильно зависеть от конечного типа объекта.Теперь, чтобы оптимизировать вещи, иногда полезно иметь возможность выполнять код параллельно (обратите внимание, я не использую слово асинхронный), если это возможно и / или необходимо.
Некоторые примеры:
До async / await у нас было два решения:
Async / await - это всего лишь распространенная модель программирования, основанная на концепции Task . Это немного проще в использовании, чем потоки или пулы потоков для задач, связанных с процессором, и намного проще в использовании, чем старая модель Begin / End. Под прикрытием, однако, это «просто» супер сложная полнофункциональная оболочка на обоих.
Таким образом, реальный выигрыш в основном связан с задачами IO Bound , которые не используют ЦП, но async / await - это всего лишь модель программирования, она не помогает вам определить, как / где произойдет обработка в конце.
Это означает, что это не потому, что у класса есть метод «DoSomethingAsync», возвращающий объект Task, который можно предположить, что он будет привязан к процессору (что означает, что он может быть совершенно бесполезным , особенно если у него нет параметра токена отмены) или IO Bound (что означает, что это, вероятно, обязательно ), или комбинация обоих (поскольку модель довольно вирусна, связи и потенциальные выгоды могут, в конце концов, быть супер смешанными и не столь очевидными)
Итак, возвращаясь к моим примерам, выполнение операций записи с использованием async / await в MemoryStream останется привязанным к процессору (я, вероятно, не получу от этого пользы), хотя я, несомненно, выиграю от этого с файлами и сетевыми потоками.
источник
Обобщая другие ответы:
Async / await в первую очередь создается для задач, связанных с вводом-выводом, поскольку, используя их, можно избежать блокировки вызывающего потока. Их основное использование - потоки пользовательского интерфейса, где нежелательно блокировать поток при операции ввода-вывода.
Async не создает свой собственный поток. Поток вызывающего метода используется для выполнения асинхронного метода, пока он не найдет ожидаемое. Затем тот же поток продолжает выполнять остальную часть вызывающего метода после вызова асинхронного метода. В вызываемом асинхронном методе после возврата из ожидаемого продолжения можно выполнить в потоке из пула потоков - единственное место, в котором появляется отдельный поток.
источник
Я пытаюсь объяснить это снизу вверх. Может быть, кто-то найдет это полезным. Я был там, сделал это, заново изобрел это, когда делал простые игры в DOS на Паскале (старые добрые времена ...)
Итак ... В каждом приложении, управляемом событиями, есть цикл обработки событий, который выглядит примерно так:
Каркасы обычно скрывают эту деталь от вас, но она есть. Функция getMessage считывает следующее событие из очереди событий или ожидает, пока событие не произойдет: перемещение мыши, нажатие клавиши, нажатие клавиши, щелчок и т. Д. И затем dispatchMessage отправляет событие соответствующему обработчику события. Затем ожидает следующего события и так далее, пока не произойдет событие выхода, которое выходит из цикла и завершает приложение.
Обработчики событий должны работать быстро, чтобы цикл обработки событий мог опрашивать больше событий, а пользовательский интерфейс оставался отзывчивым. Что произойдет, если нажатие кнопки запускает такую дорогостоящую операцию, как эта?
Пользовательский интерфейс перестает отвечать на запросы до тех пор, пока не завершится 10-секундная операция, когда элемент управления остается в функции. Чтобы решить эту проблему, вам нужно разбить задачу на небольшие части, которые можно выполнить быстро. Это означает, что вы не можете обработать все это в одном событии. Вы должны выполнить небольшую часть работы, а затем опубликовать другое событие в очереди событий, чтобы запросить продолжение.
Таким образом, вы бы изменили это на:
В этом случае выполняется только первая итерация, затем она отправляет сообщение в очередь событий для запуска следующей итерации и возвращает. В нашем примере
postFunctionCallMessage
псевдо-функция помещает событие «вызов этой функции» в очередь, поэтому диспетчер событий будет вызывать его, когда достигнет. Это позволяет обрабатывать все другие события графического интерфейса при непрерывном выполнении фрагментов длительной работы.Пока выполняется эта длительная задача, ее событие продолжения всегда находится в очереди событий. Таким образом, вы в основном изобрели свой собственный планировщик задач. Где события продолжения в очереди - это «процессы», которые работают. На самом деле это то, что делают операционные системы, за исключением того, что отправка событий продолжения и возврат в цикл планировщика выполняется через прерывание таймера ЦПУ, когда ОС зарегистрировала код переключения контекста, поэтому вам не нужно об этом заботиться. Но здесь вы пишете свой собственный планировщик, поэтому вам нужно заботиться об этом - пока.
Таким образом, мы можем запускать долго выполняющиеся задачи в одном потоке параллельно с графическим интерфейсом, разбивая их на небольшие куски и отправляя события продолжения. Это общая идея
Task
класса. Он представляет часть работы, и когда вы вызываете.ContinueWith
ее, вы определяете, какую функцию вызывать как следующую часть, когда заканчивается текущая часть (и ее возвращаемое значение передается в продолжение).Task
Класс использует пул потоков, где есть цикл обработки событий в каждом потоке , ожидая , чтобы сделать части работы , подобные хочу я показал в начале. Таким образом, вы можете запускать миллионы задач параллельно, но только несколько потоков для их выполнения. Но это также сработало бы с одним потоком - при условии, что ваши задачи правильно разделены на маленькие кусочки, каждый из которых работает в parellel.Но выполнение всей этой цепочки, разбивающей работу на мелкие части вручную, является трудоемкой работой и полностью портит структуру логики, потому что весь код фоновой задачи в основном
.ContinueWith
беспорядок. Вот где вам поможет компилятор. Это делает всю эту цепочку и продолжение для вас в фоновом режиме. Когда вы говорите,await
что говорите, скажите компилятору, что «остановитесь здесь, добавьте остальную часть функции как задачу продолжения». Компилятор позаботится обо всем остальном, так что вам не придется.источник
Фактически,
async await
цепочки являются конечным автоматом, сгенерированным компилятором CLR.async await
однако использует потоки, которые TPL использует пул потоков для выполнения задач.Причина, по которой приложение не заблокировано, заключается в том, что конечный автомат может решить, какую подпрограмму выполнить, повторить, проверить и снова принять решение.
Дальнейшее чтение:
Что генерирует async & await?
Async Await и Generated StateMachine
Асинхронный C # и F # (III.): Как это работает? - Томас Петричек
Редактировать :
Ладно. Похоже, мои разработки неверны. Однако я должен отметить, что конечные автоматы являются важными активами для
async await
s. Даже если вы принимаете асинхронный ввод-вывод, вам все равно нужен помощник, чтобы проверить, завершена ли операция, поэтому нам все еще нужен конечный автомат и определить, какая подпрограмма может выполняться асинхронно вместе.источник
Это не прямой ответ на вопрос, но я думаю, что это интересная дополнительная информация:
Async и await не создают новые темы сами по себе. НО, в зависимости от того, где вы используете асинхронное ожидание, синхронная часть ДО того, как ожидание может работать в другом потоке, чем синхронная часть ПОСЛЕ ожидания (например, ядра ASP.NET и ASP.NET ведут себя по-разному).
В приложениях на основе UI-Thread (WinForms, WPF) вы будете находиться в одном потоке до и после. Но когда вы используете асинхронное удаление в потоке пула потоков, поток до и после ожидания может не совпадать.
Отличное видео на эту тему
источник