Что делает ожидание асинхронного метода в фоновом режиме? - PullRequest
0 голосов
/ 19 февраля 2019

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

До сих пор я знал, что только Task.Run () создает новый поток.Это также верно для Task.WhenAll () или Task.WhenAny ()?

Допустим, у нас есть этот код:

    async Task<int> AccessTheWebAsync()
            {
                using (HttpClient client = new HttpClient())
                {
                    Task<string> getStringTask = client.GetStringAsync("https://docs.microsoft.com");

                    DoIndependentWork();

                    string urlContents = await getStringTask;

                    return urlContents.Length;
                }
            }

Что я ожидаю:

  1. При создании Задачи getStringTask другой поток скопирует текущий контекст и начнет выполнять метод GetStringAsync.

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

Так что я действительноне понимаю, как не создается лишний поток при ожидании Задачи.Может кто-нибудь объяснить, что именно происходит в ожидании задания?

Ответы [ 4 ]

0 голосов
/ 19 февраля 2019

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

Благородное занятие.

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

Правильно.Очень важно понимать, что await означает асинхронное ожидание . не означает "сделать эту операцию асинхронной".Это означает:

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

Если это так, при использовании await async выигрыш во времени отсутствует, поскольку не используется дополнительная нить.

Это неверно.Вы не думаете о выигрыше времени правильно.

Представьте себе этот сценарий.

  • Представьте себе мир без банкоматов.Я вырос в этом мире.Это было странное время.Поэтому в банке обычно есть люди, которые ждут, чтобы внести или снять деньги.
  • Представьте себе, что в этом банке только один кассир.
  • Теперь представьте, что банк принимает и выдает только однодолларовые банкноты.

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

  • Дайте первому человеку в строке один доллар.
  • [сделайте это десять раз]
  • Дайте второму человеку в строке одиндоллар.
  • [сделайте это десять раз]
  • Дайте третьему человеку в строке один доллар.
  • [сделайте это десять раз]
  • Дайте вамВаш доллар.

Сколько времени каждый должен ждать, чтобы получить все свои деньги?

  • Человек один ждет 10 единиц времени
  • Человек два ждет 20
  • Человек три ждет 30
  • Вы ждете 31.

Это синхронный алгоритм.Асинхронный алгоритм:

  • Дайте первому человеку в строке один доллар.
  • Дайте второму человеку в строке один доллар.
  • Дайте третьему человекув строке один доллар.
  • дайте вам свой доллар.
  • дайте первому человеку в строке один доллар.
  • ...

Это асинхронное решение.Теперь, сколько времени все ждут?

  • Каждый, получающий десять долларов, ждет около 30.
  • Вы ждете 4 единицы.

Средняя пропускная способность для больших рабочих мест ниже, но средняя пропускная способность для небольших рабочих мест намного выше . Это победа. Кроме того, время до первого доллара для каждого меньше в асинхронном рабочем процессе, даже если время до последнего доллара вышедля больших работ.Кроме того, асинхронная система ярмарка ;каждая работа ожидает приблизительно (размер задания) x (количество заданий).В синхронной системе некоторые задания почти не ждут времени, а некоторые - очень долго.

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

Это также верно для Task.WhenAll () или Task.WhenAny ()?

Они не создают темы.Они просто берут кучу задач и завершают, когда все / любые задачи выполнены.

При создании задачи getStringTask другой поток скопирует текущий контекст и начнет выполнять метод GetStringAsync.

Абсолютно нет.Задача уже асинхронна , и, поскольку это задача ввода-вывода, ей не нужен поток.Оборудование ввода-вывода уже асинхронно.Новых рабочих не нанято.

В ожидании getStringTask мы увидим, завершил ли другой поток свою задачу

Нет, другого потока нет.Мы видим, выполнило ли оборудование IO свою задачу.Там нет темы.

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

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

СноваНет другой темы.

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

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

Опять же, подумайте о каждом разе в вашей жизни, когда вы перестали выполнять задачу, потому что были заблокированы, что-то сделалиеще какое-то время, а затем снова начал делать первое задание, когда тебя разблокировали. Вам пришлось нанять рабочего? Конечно, нет.Тем не менее, каким-то образом вам удалось приготовить яйца, пока тост был в тостере.Асинхронность, основанная на задачах, просто превращает этот реальный рабочий процесс в программное обеспечение.

Меня не перестает удивлять то, как ваши дети сегодня своей странной музыкой ведут себя так, будто потоки существуют всегда, и у нет другого способа сделать многозадачность.Я научился программировать в операционной системе, в которой не было потоков .Если вы хотели, чтобы две вещи происходили одновременно, вы должны были построить свою собственную асинхронность;это не было встроено в язык или ОС.И все же мы справились.

Совместная однопотоковая асинхронность - это возвращение к миру, каким он был до того, как мы допустили ошибку, представив потоки как структуру потока управления;более элегантный и гораздо более простой мир. Ожидание - это точка приостановки в кооперативной многозадачной системе. В Windows с предварительным потоком для этого нужно было бы позвонить Yield(), и у нас не было языковой поддержки для создания продолжений и замыканий;вы хотели, чтобы состояние сохранялось через выход, вы написали код для этого.У всех вас это просто!

Может кто-нибудь объяснить, что именно происходит в ожидании Задачи?

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

Я просто хочу кое-что подтвердить.Всегда ли случается, что при ожидании задачи нет созданного потока?

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

int foo = await FooAsync();

, вы должны мысленно увидеть:

Task<int> task = FooAsync();
if (task is not already completed) 
   set continuation of task to go to "resume" on completion
   return;
resume: // If we get here, task is completed
int foo = task.Result;

Вызов метода с ожиданием не является особым видом вызова. «Ожидание» не раскручивает поток или что-то в этом роде.Это оператор, который работает с возвращенным значением.

То есть в ожидании задания не раскручивает поток.Ожидание задачи (1) проверяет, завершена ли задача, и (2), если это не так, назначает оставшуюся часть метода как продолжение задачи и возвращает результат.Это все. Await ничего не делает для создания темы.Теперь, возможно, вызванный метод раскручивает поток;это это бизнес .Это не имеет ничего общего с await , потому что ожидание происходит только после возврата вызова. Вызываемая функция не знает, что ее возвращаемое значение ожидается .

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

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

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

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

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

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

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

Я также знаю, что мы используем Task.Run () для выполнения ЦПсвязанный код для поиска доступного потока в пуле потоков.Это правда?

Это верно.Если у вас есть синхронный метод, и вы знаете, что он привязан к процессору, и вы хотите, чтобы он был асинхронным, и вы знаете, что метод безопасен для запуска в другом потоке, тогда Task.Run найдет рабочий поток, расписаниеделегат, который будет выполнен в рабочем потоке, и даст вам задачу, представляющую асинхронную операцию.Вы должны делать это только с методами, которые (1) очень долго работают, например, более 30 миллисекунд, (2) связаны с процессором, (3) безопасны для вызова в другом потоке.

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

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

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

0 голосов
/ 19 февраля 2019

Статья, которая очень помогла мне понять асинхронное ожидание, - это это интервью с Эриком Липпертом , где он сравнивает асинхронное ожидание с поваром, готовящим завтрак.Найдите где-то посередине асинхронное ожидание.

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

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

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

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

Console.Writeline(Thread.CurrentThread.ManagedThreadId);

// async call to the text reader to read a line; don't await
var taskReadLine = myTextReader.ReadLineAsync()

// because I did not await, the following will be executed as soon as a thread is free
Console.Writeline(Thread.CurrentThread.ManagedThreadId);
...

// we need the read line; await for it
string readLine = await taskReadLine;
Console.Writeline(Thread.CurrentThread.ManagedThreadId);
ProcessReadLine(readLine);

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

Ваш код не должен зависеть от какого-либо оператора в асинхронной функции, которая будет выполняться до того, как вы дождетесь результата:

async Task<int> DoIt()
{
    this.X = 4;
    await DoSomethingElseAsync(this.X);
    return 5;
}
async Task CallDoItAsync()
{
    this.X = 0;
    var taskDoIt = DoIt();

    // you didn't await, it is not guaranteed that this.X already changed to 4
    ...
    int i = await taskDoIt();
    // now you can be certain that at some moment 4 had been assigned to this.X 

Создание объекта Task не создает поток. Создание потока довольно дорого.Поэтому у вашего процесса есть пул потоков, содержащий несколько потоков.Свободные потоки помещаются в пул и могут выполнять другие действия по запросу.Как только ваш процесс нуждается в потоке, он берет доступный поток из пула потоков и планирует его запуск.

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

Вы можете получить доступ к пулу потоков, используя статический класс ThreadPool.

ThreadPool.GetMaxThreads (out int workerThreads, out int completionPortThreads);
++workerThreads;
++completionPortThreads;
bool success = ThreadPool.SetMaxThreads (workerThreads, completionPortThreads);

Будьте очень осторожны при изменении пула потоков!

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

Не асинхронно:

void CopyFile(FileInfo infile, FileInfo outFile)
{
     using(var textReader = inFile.OpenText())
     {
        using (var textWriter = outFile.CreateText())
        {
            // Read a line. Wait until line read
            var line = textReader.ReadLine();
            while (line != null)
            {
                // Write the line. Wait until line written
                textWrite.WriteLine(line);

                // Read the next line. Wait until line read
                line = textReader.ReadLine();
            }
        }
    }
}

Вы видите все ожидания.К счастью, TextReader и TextWriter выполняют буферизацию данных, в противном случае нам действительно пришлось ждать, пока данные не будут записаны, прежде чем будет прочитана следующая строка

async Task CopyFileAsync(FileInfo infile, FileInfo outFile)
{
     using(var textReader = inFile.OpenText())
     {
        using (var textWriter = outFile.CreateText())
        {
            // Read a line. Wait until line read
            var line = await textReader.ReadLineAsync();
            while (line != null)
            {
                // Write the line. Don't wait until line written
                var writeTask = textWrite.WriteLineAsync(line);

                // While the line is being written, I'm free to read the next line. 
                line = textReader.ReadLine();

                // await until the previous line has been written:
                await writeTask;
            }
        }
    }
}

Пока пишется строка, мы уже пытаемся прочитать следующую строку,Это может улучшить скорость обработки.

0 голосов
/ 19 февраля 2019

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

Это правильно.Сами по себе async и await напрямую не используют потоки.Их цель - освободить вызывающий поток .

До сих пор я знал, что только Task.Run () создает новый поток.Это также верно для Task.WhenAll () или Task.WhenAny ()?

Нет;ни Task.WhenAll, ни Task.WhenAny напрямую не используют никакие потоки.

При создании задачи getStringTask другой поток скопирует текущий контекст и начнет выполнять метод GetStringAsync.

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

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

Закрыть, за исключением того, что нет другого потока.await getStringTask проверит, выполнено ли задание ;если это не так, то он вернет незавершенное задание из AccessTheWebAsync.

Может кто-нибудь объяснить, что именно происходит в ожидании задания?

Рекомендую прочитать мой async intro для более подробной информации.

0 голосов
/ 19 февраля 2019

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

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

Итак, давайте рассмотрим ваши актуальные вопросы.


До сих пор я знал, что только Task.Run () создает новый поток.Это также верно для Task.WhenAll () или Task.WhenAny ()?

Нет, Task.WhenAll не создаст никаких новых потоков.Он будет ожидать завершения уже существующих задач независимо от того, где они выполняются (и независимо от того, выполняются ли они в каком-либо потоке !).

Задача, созданная Task.WhenAll, являетсяне работает ни в каком конкретном потоке!Он просто определяет, когда основные задачи завершены, и после того, как все они готовы, завершает себя тоже.Task.WhenAll для этого не нужен поток.


При создании задачи getStringTask другой поток скопирует текущий контекст и начнет выполнять метод GetStringAsync.

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


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

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