Если async-await не создает никаких дополнительных потоков, то как это делает приложения отзывчивыми?

снова и снова, я вижу, что он сказал, что с помощью async-await не создает дополнительных потоков. Это не имеет смысла, потому что единственный способ, которым компьютер может делать больше, чем 1 вещь за раз, - это

  • на самом деле делает больше, чем 1 вещь за раз (выполняется параллельно, используя несколько процессоров)
  • имитируя его, планируя задачи и переключаясь между ними (сделайте немного A, немного B, немного A, так далее.)

если async-await не делает ни того, ни другого, то как он может сделать приложение отзывчивым? Если есть только 1 поток, то вызов любого метода означает ожидание завершения метода перед выполнением чего-либо еще, а методы внутри этого метода должны ждать результата перед продолжением и т. д.

9 ответов


на самом деле, async/await не так волшебно. Полная тема довольно широкая, но для быстрого, но достаточно полного ответа на ваш вопрос я думаю, что мы можем справиться.

давайте рассмотрим простое событие нажатия кнопки в приложении Windows Forms:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

я явно не говорить о том, что он GetSomethingAsync возвращается сейчас. Скажем так, это то, что будет после, скажем, 2 считанные секунды.

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

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

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

этот цикл постоянно спрашивает windows "кто-нибудь что-то сделал, например, переместил мышь, нажал на что-то? Мне нужно что-то перекрасить? Если так, скажи мне!"а потом обрабатывает это "что-то". Этот цикл получил сообщение о том, что пользователь нажал на "button1" (или эквивалентный тип сообщения из Windows), и в конечном итоге вызвал наш button1_Click выше способ. Пока этот метод не вернется, этот цикл теперь застрял в ожидании. Это занимает 2 секунды, и во время этого сообщения не обрабатываются.

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

так, если в первом примере, async/await не создает новые потоки, как он это делает?

Ну, что происходит, так это то, что ваш метод разделен на два. Это один из тех широких тематических типов вещей, поэтому я не буду вдаваться в подробности, но достаточно сказать, что метод разделен на эти две вещи:

  1. весь код, ведущий к await, включая вызов GetSomethingAsync
  2. весь код ниже await

иллюстрации:

code... code... code... await X(); ... code... code... code...

переставить:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

в основном метод выполняется следующим образом:

  1. он выполняет все, что до await
  2. он называет GetSomethingAsync метод, который делает свое дело и возвращает что-то, что завершит 2 секунды в будущем

    до сих пор мы все еще внутри исходного вызова button1_Click, происходящего на основной поток, вызываемый из цикла сообщений. Если код ведет к await занимает много времени, пользовательский интерфейс все равно замерзнет. В нашем примере, не так много

  3. какого await ключевое слово, вместе с некоторой умной магией компилятора, делает это, что это в основном что-то вроде "ок, вы знаете, что, я собираюсь просто вернуться из обработчика событий нажатия кнопки здесь. Когда ты (то, чего мы ждем) соберешься закончить, дай мне знать, потому что я ... еще осталось выполнить код".

    на самом деле, пусть класс SynchronizationContext знайте, что это сделано, что, в зависимости от фактического контекста синхронизации, который находится в игре прямо сейчас, будет стоять в очереди на выполнение. Класс контекста, используемый в программе Windows Forms, будет помещать его в очередь с помощью очереди, которую перекачивает цикл сообщений.

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

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

  5. 2 секунд спустя, то, что мы ждем, завершается, и что происходит сейчас, это то, что он (ну, контекст синхронизации) помещает сообщение в очередь, что сообщение loop смотрит, говоря: "Эй, у меня есть еще один код для вас", и этот код-это весь код после ожидания.
  6. когда цикл сообщений попадает в это сообщение, он в основном "повторно вводит" этот метод, где он остановился, сразу после await и продолжить выполнение остальной части метода. Обратите внимание, что этот код снова вызывается из цикла сообщений, поэтому, если этот код делает что-то длинное без использования async/await правильно, он снова заблокирует цикл сообщений

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


хорошо, так что если GetSomethingAsync раскручивает нить, которая завершится через 2 секунды? Да, тогда, очевидно, есть новая нить в игре. Этот поток, однако, не , потому что асинхронности этого метода, это потому, что программист этого метода выбрал поток для реализации асинхронного кода. Почти все асинхронные I / O не используйте поток, они используют разные вещи. async/await сами по себе не раскручивайте новые потоки, но, очевидно, "то, чего мы ждем", может быть реализовано с помощью потоков.

в .NET есть много вещей, которые не обязательно запускают поток самостоятельно, но все еще асинхронны:

  • веб-запросы (и многие другие связанные с сетью вещи, которые требуют времени)
  • асинхронное чтение и запись файлов
  • и многое другое, хороший знак, если класс / интерфейс, о котором идет речь, имеет методы с именем SomethingSomethingAsync или BeginSomething и EndSomething и IAsyncResult участвует.

обычно эти вещи не используют нить под капотом.


хорошо, так вы хотите некоторые из этих "широких тем"?

Ну, давайте спросим Попробуйте Roslyn о нашей кнопки:

Попробуйте Roslyn

я не собираюсь связываться в полном сгенерированном классе здесь, но это довольно кровавый материал.


единственный способ, которым компьютер может делать больше, чем 1 вещь за раз, - это (1) фактически делать больше, чем 1 вещь за раз, (2) имитировать его, планируя задачи и переключаясь между ними. Поэтому, если async-await не делает ни того, ни другого

это не то, что ждетни тех. Помните, цель await не сделать синхронный код асинхронным магически. Это позволит используя то же самое методы, которые мы используем для записи синхронного кода при вызове асинхронного кода. Жду около сделать код, использующий операции с высокой задержкой, похожим на код, использующий операции с низкой задержкой. Эти операции с высокой задержкой могут быть в потоках, они могут быть на специальном оборудовании, они могут разрывать свою работу на маленькие кусочки и помещать ее в очередь сообщений для обработки потоком пользовательского интерфейса позже. Они делают что-то в достичь асинхронности, но они это те, кто это делает. Await просто позволяет вам воспользоваться этой асинхронностью.

кроме того, я думаю, что вам не хватает третьего варианта. Мы, старики - сегодняшние дети с их рэп-музыкой должны убраться с моей лужайки и т. д.-помним мир окон в начале 1990-х. Не было ни многопроцессорных машин, ни планировщиков потоков. Вы хотели запустить два приложения Windows одновременно, вы должны были доходность. Мультизадачность кооператива. ОС сообщает процессу, что он запускается, и если он плохо себя ведет, он лишает все остальные процессы обслуживания. Он бежит, пока не уступит, и каким-то образом он должен знайте, как забрать, где он остановился в следующий раз, когда ОС руки управления обратно к нему. Однопоточный асинхронный код очень похож на этот, с "await "вместо"yield". Ожидание означает: "Я собираюсь вспомнить, где я остановился здесь, и пусть кто-то другой побегает некоторое время; позвоните мне когда задачу я жду, и я забрать, где я остановился."Я думаю, вы можете видеть, как это делает приложения более отзывчивыми, как это было в Windows 3 days.

вызов любого метода означает ожидание завершения метода

есть ключ, которого вам не хватает. метод может вернуться до завершения его работы. В этом суть асинхронности. Метод возвращает, он возвращает задачу, которая означает "эта работа продолжается; скажите мне, что делать, когда она будет завершена". Работа метода не выполнена,даже если он вернулся.

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


Я объясняю это полностью в своем блоге There Is No Thread.

таким образом, современные системы ввода-вывода широко используют DMA (прямой доступ к памяти). Есть специальные, выделенные процессоры на сетевых картах, видеокартах, контроллерах HDD, последовательных / параллельных портах и т. д. Эти процессоры имеют прямой доступ к шине памяти и обрабатывают чтение/запись полностью независимо от процессора. CPU просто нужно уведомить устройство о местоположении в памяти, содержащей данные, и затем может делать свое дело, пока устройство не поднимет прерывание, уведомляющее процессор, что чтение/запись завершена.

Как только операция находится в полете, для процессора нет работы, и, следовательно, нет потока.


Я очень рад, что кто-то задал этот вопрос, потому что долгое время я также считал, что потоки необходимы для параллелизма. Когда я впервые увидел петли событие, Я думал, что это ложь. Я подумал: "этот код не может быть параллельным, если он работает в одном потоке". Имейте в виду, что это после

после исследования мой собственный, я, наконец, нашел недостающий кусок: select(). В частности, мультиплексирование ввода-вывода, реализуемое различными ядрами под разными именами:select(), poll(), epoll(), kqueue(). Это


await и async использовать задачи не потоки.

платформа имеет пул потоков, готовых выполнить некоторую работу в виде задание объекты; отправка задание для пула означает выбор свободного,уже существующие1, поток для вызова задачи метод действия.
Создание задание дело в создании нового объекта, намного быстрее, чем создание новый поток.

дали задание можно добавить продолжение это новый задание объект для выполнения как только нить закончится.

С async/await использовать заданиеs они не создать новый нить.


в то время как техника программирования прерываний широко используется в каждой современной ОС, я не думаю, что они здесь уместно.
Вы можете иметь два CPU bonded задачи выполнение параллельно (фактически перемежается) в одном процессоре с использованием aysnc/await.
Это не может быть объяснено просто тем, что ОС поддерживает очередь IORP.


последний раз, когда я проверял компилятор transformed async методы DFA работа делится на этапы, каждый из них заканчивается await инструкция.
The await начинается задание и прикрепить его продолжение выполнения следующий шаг.

в качестве примера концепции, вот пример псевдо-кода.
Вещи были упрощены для ясности и потому что я не помню все детали.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

он превращается во что-то вроде этого

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 фактически пул может иметь свою политику создания задач.


Я не собираюсь конкурировать с Эриком Липпертом или Лассе В. Карлсеном и другими, я просто хотел бы обратить внимание на другой аспект этого вопроса, который, я думаю, не был явно упомянут.

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

вы должны написать ваши ожидаемый метод специально, чтобы он либо порождал новый поток, либо использовал что-то вроде порта завершения (который вернет выполнение в текущем потоке и вызовет что-то еще для продолжения, когда порт завершения получает сигнал). Но эта часть хорошо объяснена в других ответах.


вот как я смотрю на все это, это может быть не супер технически точным, но это помогает мне, по крайней мере :).

есть в основном два типа обработки (вычисления), которые происходят на машине:

  • обработка, которая происходит на CPU
  • обработка, которая происходит на других процессорах (GPU, сетевая карта и т. д.), назовем их ИО.

Итак, когда мы пишем кусок кода, после компиляции, в зависимости от объекта мы используем (и это очень важно), обработка будет CPU bound или IO bound, и на самом деле, это может быть связано с комбинацией обоих.

примеры:

  • если я использую метод Write FileStream объект (который является потоком), обработка будет, скажем, 1% CPU bound и 99% IO bound.
  • если я использую метод Write NetworkStream объект (который является потоком), обработка будет, скажем, 1% CPU bound и 99% IO связанный.
  • если я использую метод Write Memorystream объект (который является потоком), обработка будет 100% CPU.

Итак, как видите, с объектно-ориентированной точки зрения программиста, хотя я всегда к Stream объект, то, что происходит ниже, может сильно зависеть от конечного типа объекта.

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

примеры:

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

до async / await у нас было два решения для этого:

  • темы. Он был относительно прост в использовании, с Классы Thread и ThreadPool. потоки связаны только с процессором.
  • "старые" Begin/End / AsyncCallback асинхронная модель программирования. Это просто модель, она не говорит вам, будете ли вы привязаны к CPU или IO. Если вы посмотрите на классы сокетов или FileStream, это IO bound, что круто, но мы редко используем его.

асинхронный / await-это только общая модель программирования, основанная на концепции задач. Это немного проще использовать чем потоки или пулы потоков для задач, связанных с ЦП, и намного проще в использовании, чем старая модель начала/конца. Под прикрытием, однако, это" просто " супер сложная функция-полная обертка на обоих.

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

это означает, что это не потому, что класс имеет метод "DoSomethingAsync" возвращает объект задачи, который вы можете предположить, что он будет привязан к процессору (что означает, что это может быть совсем бесполезно, особенно если у него нет параметра токена отмены) или привязки ввода-вывода (что означает, что это, вероятно,должны), или сочетание обоих (поскольку модель довольно вирусная, склеивание и потенциальные преимущества могут быть, в конце концов, супер смешанными и не столь очевидными).

Итак, возвращаясь к моим примерам, выполняя мои операции записи, используя async/await на MemoryStream останется привязанным к процессору (я, вероятно, не выиграю от этого), хотя я, безусловно, выиграю от этого с файлами и сетевыми потоками.


на самом деле async await цепочки-это государственная машина, генерируемая компилятором CLR.

async await однако использует потоки, которые TPL использует пул потоков для выполнения задач.

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

читайте далее:

что генерирует async & await?

асинхронное ожидание и Созданный StateMachine

асинхронный C# и F# (III.): Как это работает? - И Tomas Petricek

редактировать:

- ладно. Похоже, мои объяснения неверны. Однако я должен отметить, что государственные машины являются важными активами для async awaits. Даже если вы принимаете асинхронный ввод-вывод, вам все равно нужен помощник, чтобы проверить, завершена ли операция, поэтому нам все еще нужна машина состояния и определить какую рутину можно выполнять вместе.


обобщение других ответов:

Async / await в основном создается для связанных с IO задач, так как с их помощью можно избежать блокировки вызывающего потока.

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

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