Любая разница между "ждут Task.Run(); return;" и "return Task.Run()"?
Существует ли какая-либо концептуальная разница между следующими двумя частями кода:
async Task TestAsync()
{
await Task.Run(() => DoSomeWork());
}
и
Task TestAsync()
{
return Task.Run(() => DoSomeWork());
}
Различается ли сгенерированный код:?
EDIT: Чтобы избежать путаницы с Task.Run
, аналогичный случай:
async Task TestAsync()
{
await Task.Delay(1000);
}
и
Task TestAsync()
{
return Task.Delay(1000);
}
ПОСЛЕДНИЕ ОБНОВЛЕНИЯ: В дополнение к принятому ответу также существует разница в том, как обрабатывается LocalCallContext
: CallContext.LogicalGetData восстанавливается даже там, где нет асинхронности. Почему?
Ответы
Ответ 1
Обновлено, помимо различий в поведении распространения исключений, описанных ниже, есть еще несколько несколько тонких различий: версия async
/await
более подвержена блокировке блокировки при нестандартном синхронизации. Например, следующее будет заблокировано в приложении WinForms или WPF:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait(); // dead-lock here
}
Измените его на не-асинхронную версию, и он не будет заблокирован:
Task TestAsync()
{
return Task.Delay(1000);
}
Характер мертвого замка хорошо объяснен Стивеном Клири в его blog.
Другое существенное отличие заключается в распространении исключений. Исключение, созданное внутри метода async Task
, сохраняется в возвращаемом объекте Task
и остается бездействующим до тех пор, пока задача не будет обнаружена через await task
, task.Wait()
, task.Result
или task.GetAwaiter().GetResult()
. Он распространяется таким образом, даже если он выведен из синхронной части метода async
.
Рассмотрим следующий код, где OneTestAsync
и AnotherTestAsync
ведут себя совершенно по-другому:
static async Task OneTestAsync(int n)
{
await Task.Delay(n);
}
static Task AnotherTestAsync(int n)
{
return Task.Delay(n);
}
// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
// start the task
task = whatTest(n);
// do some other stuff,
// while the task is pending
Console.Write("Press enter to continue");
Console.ReadLine();
task.Wait();
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
}
Если я вызываю DoTestAsync(OneTestAsync, -2)
, он выдает следующий вывод:
Press enter to continue
Error: One or more errors occurred.await Task.Delay
Error: 2nd
Заметьте, мне пришлось нажать Enter, чтобы увидеть его.
Теперь, если я назову DoTestAsync(AnotherTestAsync, -2)
, рабочий процесс кода внутри DoTestAsync
будет совсем другим, и это тоже результат. На этот раз меня не просили нажать Enter:
Error: The value needs to be either -1 (signifying an infinite timeout), 0 or a positive integer.
Parameter name: millisecondsDelayError: 1st
В обоих случаях Task.Delay(-2)
бросает в начале, проверяя его параметры. Это может быть сценарий с готовым графиком, но теоретически Task.Delay(1000)
может также выдаваться, например, при сбое основного API-интерфейса системы.
В боковом примечании логика распространения ошибок еще отличается для методов async void
(в отличие от методов async Task
). Исключение, созданное внутри метода async void
, будет немедленно повторно вставлено в текущий контекст синхронизации потока (через SynchronizationContext.Post
), если текущий поток имеет один (SynchronizationContext.Current != null)
. В противном случае он будет перезаписан через ThreadPool.QueueUserWorkItem
). У вызывающего абонента нет возможности обработать это исключение в том же стеке стека.
Я добавил несколько подробнее о поведении обработки исключений TPL здесь и здесь.
Q: возможно ли имитировать поведение распространения исключений методов async
для не-асинхронных Task
методов, так что последнее не бросает один и тот же стек кадров?
A: если это действительно необходимо, то да, есть трюк для этого:
// async
async Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
// ...
return 42 + arg;
}
// non-async
Task<int> MethodAsync(int arg)
{
var task = new Task<int>(() =>
{
if (arg < 0)
throw new ArgumentException("arg");
// ...
return 42 + arg;
});
task.RunSynchronously(TaskScheduler.Default);
return task;
}
Обратите внимание, однако, при определенных условиях (например, когда он слишком глубоко в стеке), RunSynchronously
все равно может выполняться асинхронно.
Ответ 2
В чем разница между
async Task TestAsync()
{
await Task.Delay(1000);
}
и
Task TestAsync()
{
return Task.Delay(1000);
}
?
Я смущен этим вопросом. Позвольте мне попытаться уточнить, отвечая на ваш вопрос другим вопросом. Какая разница между?
Func<int> MakeFunction()
{
Func<int> f = ()=>1;
return ()=>f();
}
и
Func<int> MakeFunction()
{
return ()=>1;
}
?
Какая разница между двумя моими вещами, та же разница между вашими двумя вещами.
Ответ 3
-
Первый метод даже не компилируется.
Так как 'Program.TestAsync()
' является асинхронным методом, который возвращает 'Task
', ключевое слово return не должно сопровождаться выражением объекта. Вы намеревались вернуть 'Task<T>
'?
Это должно быть
async Task TestAsync()
{
await Task.Run(() => DoSomeWork());
}
-
Существует большая концептуальная разница между этими двумя. Первый - асинхронный, второй - нет. Прочитайте Async Performance: Понимание затрат Async и Await, чтобы получить немного больше о внутренних элементах async
/await
.
-
Они генерируют другой код.
.method private hidebysig
instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74
2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73
79 6e 63 3e 64 5f 5f 31 00 00
)
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x216c
// Code size 62 (0x3e)
.maxstack 2
.locals init (
[0] valuetype SOTestProject.Program/'<TestAsync>d__1',
[1] class [mscorlib]System.Threading.Tasks.Task,
[2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
)
IL_0000: ldloca.s 0
IL_0002: ldarg.0
IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this'
IL_0008: ldloca.s 0
IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
IL_0014: ldloca.s 0
IL_0016: ldc.i4.m1
IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state'
IL_001c: ldloca.s 0
IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
IL_0023: stloc.2
IL_0024: ldloca.s 2
IL_0026: ldloca.s 0
IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&)
IL_002d: ldloca.s 0
IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
IL_0039: stloc.1
IL_003a: br.s IL_003c
IL_003c: ldloc.1
IL_003d: ret
} // end of method Program::TestAsync
и
.method private hidebysig
instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed
{
// Method begins at RVA 0x21d8
// Code size 23 (0x17)
.maxstack 2
.locals init (
[0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'()
IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int)
IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>)
IL_0012: stloc.0
IL_0013: br.s IL_0015
IL_0015: ldloc.0
IL_0016: ret
} // end of method Program::TestAsync2
Ответ 4
Два примера различаются. Когда метод помечен ключевым словом async
, компилятор генерирует машину состояний за кулисами. Это то, что несет ответственность за возобновление продолжений, как только ожидалось.
В отличие от этого, когда метод не отмечен async
, вы теряете возможность await
awaitables. (То есть, внутри самого метода, этот метод все еще может ожидать его вызывающий.) Однако, избегая ключевого слова async
, вы больше не генерируете государственную машину, которая может добавить справедливый бит накладных расходов (подъем локали к полям состояния машины, дополнительные объекты к GC).
В примерах, подобных этому, если вы можете избежать async-await
и вернуться к ожиданию напрямую, это должно быть сделано для повышения эффективности метода.
См. этот вопрос и этот ответ, который очень похож на ваш вопрос и этот ответ.