Вы должны поместить Task.Run в метод, чтобы сделать его асинхронным?

304

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

Пример 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Пример 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Если я DoWork1Async()буду ждать, будет ли код выполняться синхронно или асинхронно?

Нужно ли обернуть код синхронизации, Task.Runчтобы сделать метод ожидаемым И асинхронным, чтобы не блокировать поток пользовательского интерфейса?

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

Глупый вопрос, я уверен, но я вижу в сети примеры, когда люди ждут кода, в котором нет ничего асинхронного и не заключенного в Task.Runили StartNew.

Нил
источник
30
Разве ваш первый фрагмент не дает вам никаких предупреждений?
svick

Ответы:

587

Во-первых, давайте проясним некоторую терминологию: «асинхронный» ( async) означает, что он может вернуть управление вызывающему потоку до его запуска. В asyncметоде эти точки «доходности» являются awaitвыражениями.

Это очень отличается от термина «асинхронный», так как (неправильно) используется в документации MSDN годами для обозначения «выполняется в фоновом потоке».

Чтобы еще больше запутать проблему, asyncона сильно отличается от «ожидаемой»; Есть некоторые asyncметоды, чьи возвращаемые типы не являются ожидаемыми, и многие методы возвращают ожидаемые типы, которые не являются ожидаемыми async.

Достаточно о том, чем они не являются ; вот что они есть :

  • asyncКлючевое слово позволяет асинхронный метод (то есть, это позволяет awaitвыражения). asyncметоды могут вернуться Task, Task<T>или (если необходимо) void.
  • Любой тип, который следует за определенной моделью, может быть ожидаемым. Наиболее распространенными ожидаемыми типами являются Taskи Task<T>.

Итак, если мы сформулируем ваш вопрос следующим образом: «Как я могу выполнить операцию в фоновом потоке так, чтобы это было приемлемо», ответ будет такой Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Но этот шаблон плохой подход; см. Ниже).

Но если ваш вопрос «как создать asyncметод, который может возвращать вызывающему объекту вместо блокировки», ответ заключается в том, чтобы объявить метод asyncи использовать awaitего «уступающие» точки:

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Итак, основной шаблон вещей заключается в том, чтобы asyncкод зависел от «ожидаемых» в своих awaitвыражениях. Этими «ожидаемыми» могут быть другие asyncметоды или просто обычные методы, возвращающие ожидаемые. Обычные методы, возвращающие Task/ Task<T> могут использовать Task.Runдля выполнения кода в фоновом потоке, или (чаще) они могут использовать TaskCompletionSource<T>или один из его ярлыков ( TaskFactory.FromAsync, Task.FromResultи т. Д.). Я не рекомендую оборачивать весь метод Task.Run; Синхронные методы должны иметь синхронные подписи, и потребитель должен решать, должен ли он быть заключен в Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

У меня есть async/ awaitвступление в моем блоге; в конце несколько хороших ресурсов для продолжения. Документы MSDN для asyncтоже необычайно хороши.

Стивен Клири
источник
8
@sgnsajgon: Да. asyncметоды должны вернуться Task, Task<T>или void. Taskи Task<T>ожидаемо; voidне является.
Стивен Клири
3
На самом деле, async voidсигнатура метода будет компилироваться, это просто ужасная идея, поскольку вы теряете указатель на асинхронную задачу
IEatBagels
4
@TopinFrassi: Да, они будут компилироваться, но voidэто не ожидаемо.
Стивен Клири
4
@ohadinho: Нет, то, о чем я говорю в блоге, - это когда весь метод является просто вызовом Task.Run(как DoWorkAsyncв этом ответе). Использование Task.Runдля вызова метода из контекста пользовательского интерфейса является подходящим (как DoVariousThingsFromTheUIThreadAsync).
Стивен Клири
2
Да, точно. Допустимо использовать Task.Runдля вызова метода, но если Task.Runвокруг всего метода (или почти весь) есть код, то это анти-паттерн - просто держите этот метод синхронным и перемещайте Task.Runего на уровень выше.
Стивен Клири
22

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

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Рональд Рамос
источник
26
Почему вы используете TaskCompletionSource, а не просто возвращаете задачу, возвращаемую методом Task.Run () (и изменяете ее тело для возврата результата)?
иронично
4
Просто примечание. Метод, имеющий подпись async void, как правило, является плохой практикой и считается плохим кодом, поскольку он может довольно легко привести к тупику пользовательского интерфейса. Основным исключением являются асинхронные обработчики событий.
Джаззероки
12

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

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

True Async не обязательно предполагает использование потока для операций ввода-вывода, таких как доступ к файлу / БД и т. Д. Вы можете прочитать это, чтобы понять, почему для операции ввода-вывода не нужны потоки. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

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

Чжэн Ю
источник
Так что, если мне нужно использовать синхронный внешний API в контроллере Web API, я НЕ должен оборачивать синхронный вызов в Task.Run ()? Как вы сказали, при этом начальный поток запроса будет разблокирован, но для вызова внешнего API он использует другой поток пула. На самом деле, я думаю, что это все еще хорошая идея, потому что, действуя таким образом, теоретически можно использовать два потока пула для обработки многих запросов, например, один поток может обрабатывать много входящих запросов, а другой может вызывать внешний API для всех этих запросов?
stt106
Я согласен. Я не говорю, что вы не должны полностью оборачивать все синхронные вызовы в Task.Run (). Я просто указываю на потенциальную проблему.
Чжэн
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()это правильно. Если вы это сделаете, вы просто будете переключать темы. то есть вы разблокируете исходный поток запросов, но вы берете другой поток из пула потоков, который мог бы быть использован для обработки другого запроса. Единственный результат - это переключение контекста, когда вызов завершается с абсолютно нулевым коэффициентом усиления
Saeb Amini