Ожидание task.run vs wait С#

Я искал веб-сайт и видел много вопросов относительно task.run vs ждут async, но есть этот специфический сценарий использования, где я не совсем понимаю разницу. Сценарий довольно прост, я считаю.

await Task.Run(() => LongProcess());

против

await LongProcess());

где LongProcess - это асинхронный метод с несколькими асинхронными вызовами в нем, например, с вызовом db с ожиданием ExecuteReaderAsync().

Вопрос:

Есть ли разница между этими двумя сценариями? Любая помощь или ввод оценили, спасибо!

Ответ 1

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

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

Будьте осторожны, потому что поведение Task.Run изменится в зависимости от разрешения перегрузки. В вашем примере будет выбрана перегрузка Func<Task>, которая (правильно) дождитесь окончания LongProcess. Однако, если использовался делегат с возвратом не-задачи, Task.Run будет ждать выполнения только до первого await (обратите внимание, что таким образом TaskFactory.StartNew всегда будет вести себя, поэтому не используйте это).

Ответ 2

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

Смотрите дополнение ниже об этом одном потоке

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

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

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

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

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

Вызов, не ожидающий немедленного результата, будет выглядеть следующим образом:

private async Task MyFunction()
{
    Task<ReturnType>taskA = SomeFunctionAsync(...)
    // I don't need the result yet, I can do something else
    DoSomethingElse();

    // now I need the result of SomeFunctionAsync, await for it:
    ReturnType result = await TaskA;
    // now you can use object result
}

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

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

Ссылка на статью в конце этого ответа объясняет немного больше о контексте потока

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

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

Task<ResultType> LetSomeoneDoHeavyCalculations(...)
{
    DoSomePreparations()
    // start a different thread that does the heavy calculations:
    var myTask = Task.Run( () => DoHeavyCalculations(...))
    // now you are free to do other things
    DoSomethingElse();
    // once you need the result of the HeavyCalculations await for it
    var myResult = await myTask;
    // use myResult
    ...
}

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

Еще одна статья, которая помогла мне: Async-Await от блестящего объяснителя Стивена Клири

Ответ 3

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

private async void Button1_Click(object sender, EventArgs args)
{
    await Task.Run(async () => await LongProcessAsync());
}

против

private async void Button1_Click(object sender, EventArgs args)
{
    await LongProcessAsync();
}

Я добавил суффикс Async в название метода, чтобы соответствовать инструкциям. Я также сделал async анонимным делегатом только для удобства чтения. Затраты на создание конечного автомата ничтожны и сводятся к минимуму из-за ценности четкой передачи того, что этот Task.Run возвращает стиль обещания Task, а не синхронный старомодный Task, предназначенный для фоновой обработки с привязкой к процессору. рабочие нагрузки.

Преимущество первого подхода состоит в том, что он гарантирует, что пользовательский интерфейс останется отзывчивым. Второй подход не дает такой гарантии. Пока вы используете встроенные асинхронные API-интерфейсы платформы .NET, вероятность блокировки пользовательского интерфейса при втором подходе довольно мала. Ведь эти API реализованы экспертами. К тому моменту, когда вы начнете ожидать своих собственных асинхронных методов, все гарантии отключены. Если, конечно, ваше имя не Стивен, а ваша фамилия Тауб или Клири. Если это не так, то я уверен, что рано или поздно вы напишите такой код:

public static async Task LongProcessAsync()
{
    TeenyWeenyInitialization(); // Synchronous
    await SomeBuildInAsyncMethod().ConfigureAwait(false); // Asynchronous
    CalculateAndSave(); // Synchronous
}

Проблема, очевидно, связана с методом TeenyWeenyInitialization(). Этот метод является синхронным и предшествует первому await внутри тела асинхронного метода, поэтому его не ожидают. Он будет работать синхронно каждый раз, когда вы вызываете LongProcessAsync(). Поэтому, если вы будете следовать второму подходу (без Task.Run), TeenyWeenyInitialization() будет выполняться в потоке пользовательского интерфейса.

Насколько это может быть плохо? Инициализация крошечная в конце концов! Просто быстрая поездка в базу данных, чтобы получить значение, прочитать первую строку небольшого текстового файла, получить значение из реестра. Все кончилось за пару миллисекунд. В то время, когда вы написали программу. В вашем ПК. Перед перемещением папки с данными на общем диске. До того, как количество данных в базе данных стало огромным.

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

Наличие приложения, которое работает плохо, неприятно. Наличие приложения, которое плохо работает и замораживает пользовательский интерфейс, еще хуже. Вы действительно хотите рисковать? Если вы этого не сделаете, всегда используйте Task.Run(async внутри ваших обработчиков событий. Особенно в ожидании методов вы закодировали себя!