Подтвердить что ты не робот

Путаница в отношении потоков, и если асинхронные методы действительно асинхронны в С#

Я читал об асинхронном/ожидании и когда Task.Yield мог быть полезен и наткнулся на этот пост. У меня возник вопрос относительно ниже этого сообщения:

Когда вы используете async/wait, нет гарантии, что метод, который вы используете вызов, когда вы ожидаете FooAsync(), будет выполняться асинхронно. Внутренняя реализация может свободно возвращаться, используя полностью синхронный путь.

Это немного непонятно для меня, вероятно, потому, что определение асинхронности в моей голове не выстраивается.

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

Вопрос:

Если у меня длинная работа, связанная с ЦП (скажем, она делает много жесткой математики), то выполнение этой задачи асинхронно должно блокировать некоторые потоки правильно? Что-то должно на самом деле делать математику. Если я жду этого, то поток будет заблокирован.

Что такое пример асинхронного метода и как они будут работать? Являются ли они ограниченными для операций ввода-вывода, которые используют некоторые аппаратные возможности, поэтому нить никогда не блокируется?

4b9b3361

Ответ 1

Это немного непонятно для меня, вероятно, потому, что определение асинхронности в моей голове не выстраивается.

Хорошо, что вы просите разъяснений.

На мой взгляд, поскольку в основном я использую UI dev, асинхронный код - это код, который не запускается в потоке пользовательского интерфейса, а в другом потоке.

Эта вера распространена, но ложна. Нет требования, чтобы асинхронный код работал на любом втором потоке.

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

Где вы там наняли второго рабочего, чтобы посмотреть ваш тостер?

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

Если у меня длинная работа, связанная с ЦП (скажем, она делает много жесткой математики), то выполнение этой задачи асинхронно должно блокировать некоторые потоки правильно? Что-то должно действительно выполнять математику.

Здесь я дам вам тяжелую проблему. Здесь находится столбец из 100 номеров; пожалуйста, добавьте их вручную. Таким образом, вы добавляете первое ко второму и суммируете. Затем вы добавляете общее количество в третье и получаете общее количество. Тогда, о, черт, вторая страница цифр отсутствует. Помните, где вы были, и сделайте тосты. Ах, когда тост был тостов, письмо пришло с остальными номерами. Когда вы закончите смазывать тосты, продолжайте добавлять эти цифры и не забудьте съесть тост в следующий раз, когда у вас будет свободный момент.

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

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

Если я жду его, то поток будет заблокирован.

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

Что такое пример асинхронного метода и как они будут работать? Являются ли они ограниченными для операций ввода-вывода, которые используют некоторые аппаратные возможности, поэтому нить никогда не блокируется?

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

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

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

Ответ 2

Я читал об асинхронном/ожидании

Могу ли я рекомендовать async intro?

и когда Task.Yield может быть полезным

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

На мой взгляд, поскольку в основном я использую UI dev, асинхронный код - это код, который не запускается в потоке пользовательского интерфейса, а в другом потоке.

Асинхронный код может быть без потоков.

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

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

Если у меня длинная работа, связанная с ЦП (скажем, она делает много жесткой математики), то выполнение этой задачи асинхронно должно блокировать некоторые потоки правильно? Что-то должно действительно выполнять математику.

Да; в этом случае вы бы хотели определить, что работа с синхронным API (так как это синхронная работа), а затем вы можете вызывать его из потока пользовательского интерфейса с помощью Task.Run, например:

var result = await Task.Run(() => MySynchronousCpuBoundCode());

Если я жду его, то поток будет заблокирован.

Нет; поток пула потоков будет использоваться для запуска кода (фактически не заблокированного), а поток пользовательского интерфейса асинхронно ожидает завершения этого кода (также не заблокирован).

Что такое пример асинхронного метода и как они будут работать?

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

Ограничены ли они операциями ввода-вывода, которые используют некоторые аппаратные возможности, поэтому нить никогда не блокируется?

Не совсем, хотя операции ввода-вывода - легкие примеры. Другим довольно простым примером являются таймеры (например, Task.Delay). Хотя вы можете создать по-настоящему асинхронный API для любого типа событий.

Ответ 3

Когда вы используете async/wait, нет гарантии, что метод, который вы вызываете, когда вы ожидаете FooAsync(), будет фактически выполняться асинхронно. Внутренняя реализация может быть возвращена с использованием полностью синхронного пути.

Это немного непонятно для меня, вероятно, потому, что определение асинхронный в моей голове не выстраивается.

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

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

Рассмотрим этот код, который должен показывать оба этих пути. Если ключ находится в кеше, он возвращается синхронно. В противном случае запускается async op, который вызывает базу данных:

Task<T> GetCachedDataAsync(string key)
{
    if(cache.TryGetvalue(key, out T value))
    {
        return Task.FromResult(value); // synchronous: no awaits here.
    }

    // start a fully async op.
    return GetDataImpl();

    async Task<T> GetDataImpl()
    {
        value = await database.GetValueAsync(key);
        cache[key] = value;
        return value;
    }
}

Итак, понимая это, вы можете сделать вывод, что теоретически вызов database.GetValueAsync() может иметь похожий код и сам сможет синхронно возвращаться: так что даже ваш путь async может закончиться синхронно на 100%. Но ваш код не нуждается в заботе: async/await обрабатывает оба случая без проблем.

Если у меня длинная работа, связанная с ЦП (скажем, она делает много жесткой математики), то выполнение этой задачи асинхронно должно блокировать некоторые потоки правильно? Что-то должно на самом деле делать математику. Если я жду этого, то поток будет заблокирован.

Блокировка - это четко определенный термин - это означает, что ваш поток дал свое окно выполнения, пока он чего-то ждет (I/O, мьютекс и т.д.). Таким образом, ваш поток, выполняющий математику, не считается заблокированным: он фактически выполняет работу.

Что такое пример асинхронного метода и как они будут работать? Являются ли они ограниченными для операций ввода-вывода, которые используют некоторые аппаратные возможности, поэтому нить никогда не блокируется?

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

async Task<double> DoSomethingAsync()
{
    double x = await ReadXFromFile();

    Task<double> a = LongMathCodeA(x);
    Task<double> b = LongMathCodeB(x);

    await Task.WhenAll(a, b);

    return a.Result + b.Result;
}

Ответ 4

Эта тема довольно обширна, и могут возникнуть несколько обсуждений. Однако использование async и await в С# считается асинхронным программированием. Однако, как работает асинхронность, это общая дискуссия. До .NET 4.5 не было асинхронных и ожидающих ключевых слов, и разработчикам приходилось разрабатывать непосредственно против Task Parallel Librar y (TPL). Там разработчик полностью контролировал, когда и как создавать новые задачи и даже потоки. Однако это имело недостаток, поскольку не было действительно экспертом по этой теме, приложения могут пострадать от тяжелых проблем с производительностью и ошибок из-за условий гонки между потоками и так далее.

Начиная с .NET 4.5 были введены ключевые слова async и ожидания с новым подходом к асинхронному программированию. Асинхронные и ожидающие ключевые слова не создают дополнительные потоки. Асинхронные методы не требуют многопоточности, потому что метод async не запускается в своем потоке. Метод работает в текущем контексте синхронизации и использует время в потоке только тогда, когда метод активен. Вы можете использовать Task.Run, чтобы переместить работу, связанную с процессором, в фоновый поток, но фоновый поток не помогает с процессом, который просто ждет, когда результаты станут доступными.

Асинхронный подход к асинхронному программированию предпочтительнее существующих подходов практически в каждом случае. В частности, этот подход лучше, чем BackgroundWorker для операций с привязкой к IO, потому что код проще, и вам не нужно защищать от условий гонки. Вы можете узнать больше об этой теме ЗДЕСЬ.

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

Ответ 5

Асинхронный не предполагает Параллельный

Асинхронный означает только concurrency. Фактически даже использование явных потоков не гарантирует, что они будут выполняться одновременно (например, когда слияния потоков для одного и того же ядра или, что более часто, когда есть только одно ядро ​​в машине).

Следовательно, вы не должны ожидать, что асинхронная операция будет выполняться одновременно с чем-то другим. Асинхронный означает, что это произойдет, в конце концов, в другое время (a (грек) = без, syn (грек) = вместе, khronos (грек) = время. = > Асинхронный = не происходит одновременно).

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

Когда вы await выполняете асинхронную операцию, вы создаете concurrency (com (latin) = вместе, currere (latin) = run. = > "Concurrent" = для запуска вместе). Это потому, что вы просите, чтобы асинхронная операция достигла завершения, прежде чем двигаться дальше. Мы можем сказать, что исполнение сходится. Это похоже на концепцию объединения потоков.


Если асинхронный не может быть параллельным

Когда вы используете async/wait, нет гарантии, что метод, который вы вызываете, когда вы ожидаете FooAsync(), будет фактически выполняться асинхронно. Внутренняя реализация может быть возвращена с использованием полностью синхронного пути.

Это может произойти тремя способами:

  • Можно использовать await для всего, что возвращает Task. Когда вы получите Task, он может быть уже завершен.

    Тем не менее, это само по себе не означает, что он работал синхронно. Фактически, это предполагает, что он запускается асинхронно и завершен, прежде чем вы получите экземпляр Task.

    Помните, что вы можете await выполнить уже выполненную задачу:

    private static async Task CallFooAsync()
    {
        await FooAsync();
    }
    
    private static Task FooAsync()
    {
        return Task.CompletedTask;
    }
    
    private static void Main()
    {
        CallFooAsync().Wait();
    }
    

    Кроме того, если метод async не имеет await, он будет работать синхронно.

    Примечание. Как вы уже знаете, метод, который возвращает Task, может быть ожидающим в сети или в файловой системе и т.д.... это не означает, что вы запускаете новый Thread или помещаете что-то на ThreadPool.

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

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

    Примечание. Недавно я написал пользовательский SyncrhonizationContext, который запускает задачи в одном потоке. Вы можете найти его в Создание планировщика задач (System.Threading.Tasks.). Это приведет к такому TaskScheduler с вызовом FromCurrentSynchronizationContext.

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

    Примечание. Одним из примечательных исключений является Task, отмеченный LongRunning. LongRunning Task будет работать в отдельном потоке.


Ваш вопрос

Если у меня длинная работа, связанная с ЦП (скажем, она делает много жесткой математики), то выполнение этой задачи асинхронно должно блокировать некоторые потоки правильно? Что-то должно на самом деле делать математику. Если я жду этого, то поток будет заблокирован.

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

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

Одна из ключевых характеристик async и await заключается в том, что они принимают SynchronizationContextот вызывающего абонента. Для большинства потоков, которые приводят к использованию стандартного TaskScheduler (который, как упоминалось ранее, использует ThreasPool). Однако для потока пользовательского интерфейса это означает, что задачи отправляются в очередь сообщений, это означает, что они будут выполняться в потоке пользовательского интерфейса. Преимущество этого в том, что вам не нужно использовать Invoke или BeginInvoke для доступа к компонентам пользовательского интерфейса.

Прежде чем перейти к await a Task из потока пользовательского интерфейса без его блокировки, хочу отметить, что можно реализовать TaskScheduler, если вы await на Task, вы не блокируете свою нить или простаиваете, вместо этого вы позволяете своему потоку выбирать еще один Task, ожидающий выполнения. Когда я был backporting Tasks for.NET 2.0 Я экспериментировал с этим.

Что такое пример асинхронного метода и как они будут работать? Являются ли они ограниченными для операций ввода-вывода, которые используют некоторые аппаратные возможности, поэтому нить никогда не блокируется?

Кажется, вы запутались асинхронно, не блокируя поток. Если то, что вы хотите, является примером асинхронных операций в .NET, которые не требуют блокировки потока, способ сделать это, который вы можете легко понять, - это использовать вместо await. И для продолжений, которые нужно запустить в потоке пользовательского интерфейса, вы можете использовать TaskScheduler.FromCurrentSynchronizationContext.

Не выполняйте ожидаемое ожидание ожидания. И я имею в виду использование Timer, Application.Idle или что-то в этом роде.

Когда вы используете async, вы говорите компилятору переписать код метода таким образом, чтобы его можно было сломать. Результат аналогичен продолжениям с гораздо более удобным синтаксисом. Когда поток достигнет await, будет запланирован Task, и поток будет продолжен после текущего вызова async (из метода). Когда выполняется Task, запланировано продолжение (после await).

Для потока пользовательского интерфейса это означает, что, когда он достигнет await, он может продолжать обрабатывать сообщения. Как только ожидаемый Task будет выполнен, продолжение (после await) будет запланировано. В результате достижение await не означает блокировки потока.

Тем не менее слепо добавив async и await, не удастся устранить все ваши проблемы.

Я представляю вам эксперимент. Получите новое приложение Windows Forms, запустите Button и TextBox и добавьте следующий код:

    private async void button1_Click(object sender, EventArgs e)
    {
        await WorkAsync(5000);
        textBox1.Text = @"DONE";
    }

    private async Task WorkAsync(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }

Он блокирует пользовательский интерфейс. Случается, что, как упоминалось ранее, await автоматически использует SynchronizationContext потока вызывающего. В этом случае это поток пользовательского интерфейса. Поэтому WorkAsync будет работать в потоке пользовательского интерфейса.

Вот что происходит:

  • Нити пользовательского интерфейса получают сообщение клика и вызывают обработчик события клика
  • В обработчике событий клика поток пользовательского интерфейса достигает await WorkAsync(5000)
  • WorkAsync(5000) (и планирование его продолжения) планируется запустить в текущем контексте синхронизации, который является контекстом синхронизации потоков пользовательского интерфейса... что означает, что он отправляет сообщение для его выполнения.
  • Теперь поток пользовательского интерфейса может обрабатывать дальнейшие сообщения.
  • В потоке пользовательского интерфейса выбирается сообщение для выполнения WorkAsync(5000) и расписания его продолжения
  • поток пользовательского интерфейса вызывает WorkAsync(5000) с продолжением
  • В WorkAsync поток пользовательского интерфейса выполняется Thread.Sleep. Пользовательский интерфейс теперь безответственен в течение 5 секунд.
  • Продолжение расписания остатка обработчика событий клика для запуска, это делается путем публикации другого сообщения для потока пользовательского интерфейса.
  • Теперь поток пользовательского интерфейса может обрабатывать дальнейшие сообщения.
  • Поток пользовательского интерфейса выбирает сообщение для продолжения в обработчике событий клика
  • Пользовательский интерфейс обновляет текстовое поле

Результатом является синхронное выполнение с накладными расходами.

Да, вы должны использовать Task.Delay. Дело не в этом; рассмотрим Sleep подставку для некоторых вычислений. Дело в том, что просто использование async и await везде не даст вам приложение, которое автоматически будет параллельным. Гораздо лучше выбрать, что вы хотите запустить в фоновом потоке (например, на ThreadPool) и что вы хотите запустить в потоке пользовательского интерфейса.

Теперь попробуйте следующий код:

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => Work(5000));
        textBox1.Text = @"DONE";
    }

    private void Work(int milliseconds)
    {
        Thread.Sleep(milliseconds);
    }

Вы найдете, что ожидание не блокирует пользовательский интерфейс. Это связано с тем, что в этом случае Thread.Sleep теперь работает на ThreadPool благодаря Task.Run. И благодаря button1_Click async, когда код достигнет await, поток пользовательского интерфейса может продолжить работу. После выполнения Task код будет возобновлен после await благодаря компилятору, который переписывает метод, чтобы это точно разрешить.

Вот что происходит:

  • Нити пользовательского интерфейса получают сообщение клика и вызывают обработчик события клика
  • В обработчике событий клика поток пользовательского интерфейса достигает await Task.Run(() => Work(5000))
  • Task.Run(() => Work(5000)) (и планирование его продолжения) планируется запустить в текущем контексте синхронизации, который является контекстом синхронизации потока пользовательского интерфейса... что означает, что он отправляет сообщение для его выполнения.
  • Теперь поток пользовательского интерфейса может обрабатывать дальнейшие сообщения.
  • Лимит пользовательского интерфейса выбирает сообщение для выполнения Task.Run(() => Work(5000)) и назначает его продолжение, когда выполняется
  • Поток пользовательского интерфейса вызывает Task.Run(() => Work(5000)) с продолжением, это будет работать на ThreadPool
  • Теперь поток пользовательского интерфейса может обрабатывать дальнейшие сообщения.

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

Ответ 6

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

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

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

var a = await A();
await B(a);

Порядок событий определяется await invocations. B использует возвращаемое значение A, что означает, что A должен быть запущен до B. Метод, содержащий этот код, имеет синхронный рабочий процесс, а два метода A и B синхронны друг относительно друга.

Это очень полезно, потому что синхронные рабочие процессы, как правило, легче думать, и, что более важно, много рабочих процессов просто синхронно. Если B должен выполнить результат A, он должен работать после A 2. Если вам нужно сделать HTTP-запрос для получения URL-адреса для другого HTTP-запроса, вы должны дождаться завершения первого запроса; он не имеет ничего общего с планированием потоков/задач. Возможно, мы могли бы назвать эту "неотъемлемую синхронизацию", помимо "случайной синхронности", где вы наставляете порядок на вещи, которые не нужно заказывать.

Вы говорите:

На мой взгляд, поскольку в основном я использую UI dev, асинхронный код - это код, который не запускается в потоке пользовательского интерфейса, а в другом потоке.

Вы описываете код, который выполняется асинхронно в отношении пользовательского интерфейса. Это, безусловно, очень полезный случай для асинхронности (людям не нравится пользовательский интерфейс, который перестает отвечать). Но это всего лишь конкретный случай более общего принципа - позволяя вещам происходить не по порядку относительно друг друга. Опять же, это не абсолютное - вы хотите, чтобы некоторые события выходили из строя (скажем, когда пользователь перетаскивает окно или индикатор выполнения изменяется, окно должно все же перерисовываться), в то время как другие не должны выходить из строя (кнопка "Процесс" не нужно нажимать до завершения действия "Загрузка" ). await в этом случае не отличается от использования Application.DoEvents в принципе - он вводит многие из тех же проблем и преимуществ.

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

Во-первых, вам нужно понять, что await использует свой аргумент, как если бы это был вызов метода. В моем примере A должен быть уже вызван до того, как код, сгенерированный await, может сделать что угодно, включая "освобождение управления обратно в контур интерфейса". Возвращаемое значение A равно Task<T> вместо просто T, представляя "возможное значение в будущем" - и await -генерированный код проверяет, уже ли это значение (в этом случае оно просто продолжается в том же потоке) или нет (это означает, что мы получаем, чтобы отпустить поток обратно в цикл пользовательского интерфейса). Но в любом случае значение Task<T> должно быть возвращено из A.

Рассмотрим эту реализацию:

public async Task<int> A()
{
  Thread.Sleep(1000);

  return 42;
}

Абонент нуждается в A, чтобы вернуть значение (задача int); поскольку в методе нет await, это означает return 42;. Но это не может произойти до завершения сна, потому что две операции синхронны по отношению к потоку. Поток вызывающего абонента будет заблокирован на секунду, независимо от того, использует ли он await или нет - блокировка находится в A() сама, а не await theTaskResultOfA.

В противоположность этому, рассмотрите это:

public async Task<int> A()
{
  await Task.Delay(1000);

  return 42;
}

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

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

var responseTask = GetAsync("http://www.google.com");

// Do some CPU intensive task
ComputeAllTheFuzz();

response = await responseTask;

Нам нужно сделать какую-то работу. Некоторые события могут выполняться асинхронно по отношению к другим (в этом случае ComputeAllTheFuzz не зависит от HTTP-запроса) и являются асинхронными. Но в какой-то момент нам нужно вернуться к синхронному рабочему процессу (например, что-то, что требует как результата ComputeAllTheFuzz, так и HTTP-запроса). Точка await, которая синхронизирует выполнение снова (если у вас было несколько асинхронных рабочих процессов, вы использовали бы что-то вроде Task.WhenAll). Однако, если HTTP-запрос удалось завершить до вычисления, нет смысла выделять элемент управления в точке await - мы можем просто продолжить в том же потоке. Не было никакой потери процессора - никакой блокировки нити; он действительно полезен для работы ЦП. Но мы не дали возможности обновлять пользовательский интерфейс.

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

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

Давайте рассмотрим обработчик события UI. Если для отдельных асинхронных операций не нужен поток для выполнения (например, асинхронный ввод-вывод), часть асинхронного метода может разрешить выполнение другого кода в исходном потоке (и пользовательский интерфейс остается чувствительным в этих частях). Когда операции снова потребуется процессор/поток, может потребоваться или не потребовать, чтобы исходный поток продолжал работу. Если это произойдет, пользовательский интерфейс будет заблокирован снова на время работы ЦП; если это не так (awaiter указывает это с помощью ConfigureAwait(false)), код UI будет работать параллельно. Предполагая, что ресурсов достаточно для обработки обоих, конечно. Если вам нужно, чтобы пользовательский интерфейс оставался отзывчивым во все времена, вы не можете использовать поток пользовательского интерфейса для любого выполнения достаточно долго, чтобы быть заметным - даже если это означает, что вам нужно обернуть ненадежную "обычно асинхронную, но иногда блокировать на несколько секунд" async метод в Task.Run. Там затраты и выгоды для обоих подходов - это компромисс, как со всем инженерным:)


Ответ 7

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

public static async Task<string> Foo()
{
    Console.WriteLine("In Foo");
    await Task.Yield();
    Console.WriteLine("I'm Back");
    return "Foo";
}


static void Main(string[] args)
{
    var t = new Task(async () =>
    {
        Console.WriteLine("Start");
        var f = Foo();
        Console.WriteLine("After Foo");        
        var r = await f;
        Console.WriteLine(r);
    });
    t.RunSynchronously();
    Console.ReadLine();
}

введите описание изображения здесь

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

ПРИМЕЧАНИЕ. Никаких потоков при создании этого кода не было заблокировано:)

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

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