TL; DR
Причина, по которой вы получаете предупреждение, заключается в том, что
Task.Run(() => DoThings(10)); // warning here
возвращает задание, и поскольку ваш метод ServePage
помечен как асинхронный, компилятор считает, что вам следует дождаться результата Task
Detail
Вы смешиваете две очень разные парадигмы, которые по совпадению включают Task
, а именно:
Task.Run()
, что обычно полезно для распараллеливания работы, связанной с процессором, с использованием нескольких ядер, которые могут быть доступны
async / await
, что полезно для ожидания завершения операций, связанных с вводом / выводом, без блокировки (траты) потока.
Так, например, если вы хотите выполнить 3 операции с привязкой к ЦП одновременно, а поскольку Task.Run
возвращает Task
, вы можете сделать следующее:
public Page ServePage() // If we are CPU bound, there's no point decorating this as async
{
var taskX = Task.Run(() => CalculateMeaningOfLife()); // Start taskX
var taskY = Task.Run(() => CalculateJonSkeetsIQ()); // Start taskY
var z = DoMoreHeavyLiftingOnCurrentThread();
Task.WaitAll(taskX, taskY); // Wait for X and Y - the Task equivalent of `Thread.Join`
// Return a final object comprising data from the work done on all three tasks
return new Page(taskX.Result, taskY.Result, z);
}
Выше, вероятно, потребуются три потока, которые могли бы одновременно выполнять работу с процессором, если для этого достаточно ядер.
Это в отличие от async / await
, который обычно используется для освобождения потоков при ожидании завершения вызовов, связанных с вводом / выводом. Предполагая, что DoThings
действительно связан с вводом / выводом и выглядит примерно так:
public async Task<string> DoThings(int foo) {
var result = await SomeAsyncIo(foo);
return "done!";
}
Параллельно, но все же, асинхронно, вы можете сделать:
public async Task<Page> ServePage() {
var task1 = DoThings(123); // Kick off Task 1
var task2 = DoThings(234); // Kick off Task 2 in parallel with task 1
await Task.WhenAll(task1, task2); // Wait for both tasks to finish, while releasing this thread
return new Page(task1.Result, task2.Result); // Return a result with data from both tasks
}
Если работа, связанная с вводом / выводом, занимает разумное количество времени, есть большая вероятность, что во время await Task.WhenAll
, когда потоки ZERO действительно работают, есть момент - См. Статья Стивена Клири .
Есть 3-й, но очень опасный вариант - это огонь и забыть. Поскольку метод DoThings
уже помечен как async
, он уже возвращает Task
, поэтому вообще не нужно использовать Task.Run
. Огонь и забыл бы выглядеть следующим образом:
public Page ServePage() // No async
{
#pragma warning disable 4014 // warning is suppresed by the Pragma
DoThings(10); // Kick off DoThings but don't wait for it to complete.
#pragma warning enable 4014
// ... other code
return new Page();
}
Согласно комментарию @ JohnWu, подход «забей и забудь» опасен и обычно указывает на запах дизайна. Подробнее об этом здесь и здесь
Редактировать
Re:
есть нюанс, который ускользает от меня снова и снова, например, вызов асинхронного метода, который возвращает Task из синхронного метода, запускает и забывает выполнение метода. (Это самый последний пример кода.) Я правильно понимаю?
Немного сложно объяснить, но независимо от того, вызывается ли с или без ключевого слова await
какой-либо синхронный код в вызываемом методе async
1072 * до того, как первое ожидание будет выполнено на Тема звонящего, если только мы не прибегнем к молоткам типа Task.Run
.
Возможно, этот пример может помочь пониманию (обратите внимание, что мы сознательно используем синхронный Thread.Sleep
, а не await Task.Delay
для имитации работы с привязкой к процессору и введения задержки, которая может наблюдаться)
public async Task<Page> ServePage()
{
// Launched from this same thread,
// returns after ~2 seconds (i.e. hits both sleeps)
// continuation printed.
await DoThings(10);
#pragma warning disable 4014
// Launched from this same thread,
// returns after ~1 second (i.e. hits first sleep only)
// continuation not yet printed
DoThings(10);
// Task likely to be scheduled on a second thread
// will return within few milliseconds (i.e. not blocked by any sleeps)
Task.Run(() => DoThings(10));
// Task likely to be scheduled on a second thread
// will return after 2 seconds, although caller's thread will be released during the await
// Generally a waste of a thread unless also doing CPU bound work on current thread, or unless we want to release the calling thread.
await Task.Run(() => DoThings());
// Redundant state machine, returns after 2 seconds
// see return Task vs async return await Task https://stackoverflow.com/questions/19098143
await Task.Run(async () => await DoThings());
}
public async Task<string> DoThings(int foo) {
Thread.Sleep(1000);
var result = await SomeAsyncIo(foo);
Trace.WriteLine("Continuation!");
Thread.Sleep(1000);
return "done!";
}
Следует отметить еще один важный момент - в большинстве случаев нет никаких гарантий, что код продолжения ПОСЛЕ ожидания будет выполнен в том же потоке, что и до await
. Код продолжения переписывается компилятором в Задачу, и задача продолжения будет запланирована в пуле потоков.