Czy istnieje różnica pojęciowa między następującymi dwoma fragmentami kodu:
async Task TestAsync()
{
await Task.Run(() => DoSomeWork());
}
i
Task TestAsync()
{
return Task.Run(() => DoSomeWork());
}
Czy wygenerowany kod też się różni?
EDYCJA: Aby uniknąć pomyłki z Task.Run
podobnym przypadkiem:
async Task TestAsync()
{
await Task.Delay(1000);
}
i
Task TestAsync()
{
return Task.Delay(1000);
}
PÓŹNA AKTUALIZACJA: Oprócz zaakceptowanej odpowiedzi istnieje również różnica w sposobie postępowania LocalCallContext
: CallContext.LogicalGetData jest przywracana nawet wtedy, gdy nie ma asynchronii. Czemu?
c#
async-await
avo
źródło
źródło
await
/async
w ogóle :)Odpowiedzi:
Jedna główna różnica dotyczy propagacji wyjątków. Wyjątkiem wyrzucane wewnątrz
async Task
sposobie pobiera przechowywane w zwróconymTask
obiektu i pozostaje w uśpieniu aż zadanie zostanie obserwowana przezawait task
,task.Wait()
,task.Result
itask.GetAwaiter().GetResult()
. Jest propagowany w ten sposób, nawet jeśli jest wyrzucany z synchronicznej częściasync
metody.Rozważmy następujący kod, gdzie
OneTestAsync
iAnotherTestAsync
zachowuj się zupełnie inaczej: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); } }
Jeśli zadzwonię
DoTestAsync(OneTestAsync, -2)
, generuje następujący wynik:Uwaga, musiałem nacisnąć, Enteraby to zobaczyć.
Teraz, jeśli zadzwonię
DoTestAsync(AnotherTestAsync, -2)
, przepływ pracy kodu wewnątrzDoTestAsync
jest zupełnie inny, podobnie jak dane wyjściowe. Tym razem nie poproszono mnie o naciśnięcie Enter:W obu przypadkach
Task.Delay(-2)
wrzuca na początek, podczas walidacji swoich parametrów. Może to być zmyślony scenariusz, ale teoretycznieTask.Delay(1000)
może również wystąpić, np. Gdy podstawowy systemowy interfejs API zegara zawiedzie.Na marginesie, logika propagacji błędów jest jeszcze inna w przypadku
async void
metod (w przeciwieństwie doasync Task
metod). Wyjątek zgłoszony wewnątrzasync void
metody zostanie natychmiast ponownie zgłoszony do kontekstu synchronizacji bieżącego wątku (przezSynchronizationContext.Post
), jeśli bieżący wątek go ma (SynchronizationContext.Current != null)
. W przeciwnym razie zostanie ponownie wyrzucony za pośrednictwemThreadPool.QueueUserWorkItem
). Wzywający nie ma szans na obsłużenie tego wyjątku w tej samej ramce stosu.Tutaj i tutaj zamieściłem więcej szczegółów na temat obsługi wyjątków TPL .
P : Czy można naśladować zachowanie propagacji wyjątków
async
metod dla metod innych niż asynchroniczneTask
, aby ta ostatnia nie rzucała się na tę samą ramkę stosu?O : Jeśli naprawdę potrzebujesz, to tak, jest na to sztuczka:
// 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; }
Należy jednak pamiętać, że w pewnych warunkach (np. Gdy jest zbyt głęboko na stosie),
RunSynchronously
może nadal działać asynchronicznie.Inną zauważalną różnicą jest to, że / wersja jest bardziej podatna na martwy blokowania na inny niż domyślny kontekście synchronizacji . Na przykład następujące elementy zostaną zablokowane w aplikacji WinForms lub WPF:
async
await
static async Task TestAsync() { await Task.Delay(1000); } void Form_Load(object sender, EventArgs e) { TestAsync().Wait(); // dead-lock here }
Zmień ją na wersję inną niż asynchroniczna i nie będzie się blokować:
Task TestAsync() { return Task.Delay(1000); }
Naturę zamkniętego zamka dobrze wyjaśnia Stephen Cleary na swoim blogu .
źródło
return Task.Run()
iawait Task.Run(); return
, a nieawait Task.Run().ConfigureAwait(false); return
Jestem zdezorientowany tym pytaniem. Spróbuję to wyjaśnić, odpowiadając na twoje pytanie innym pytaniem. Jaka jest różnica pomiędzy?
Func<int> MakeFunction() { Func<int> f = ()=>1; return ()=>f(); }
i
Func<int> MakeFunction() { return ()=>1; }
?
Jakakolwiek jest różnica między moimi dwiema rzeczami, ta sama różnica jest między twoimi dwiema rzeczami.
źródło
Task.Delay(1000).ContinueWith(() = {})
. W drugim jest po prostuTask.Delay(1000)
. Różnica jest nieco subtelna, ale znacząca.Pierwsza metoda nawet się nie kompiluje.
To musi być
async Task TestAsync() { await Task.Run(() => DoSomeWork()); }
Między tymi dwoma istnieje zasadnicza różnica pojęciowa. Pierwsza jest asynchroniczna, druga nie. Przeczytaj artykuł Async Performance: Understanding the Costs of Async and Await, aby uzyskać więcej informacji na temat elementów wewnętrznych
async
/await
.Generują inny kod.
.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
i
.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
źródło
Te dwa przykłady nie różnią się. Gdy metoda jest oznaczona
async
słowem kluczowym, kompilator generuje maszynę stanu za kulisami. To właśnie jest odpowiedzialne za wznowienie kontynuacji, gdy oczekiwano na coś nieoczekiwanego.W przeciwieństwie do tego, gdy metoda nie jest oznaczona
async
, tracisz możliwośćawait
oczekiwania. (Oznacza to, że w samej metodzie; metoda może nadal być oczekiwana przez obiekt wywołujący). Jednak unikającasync
słowa kluczowego, nie generujesz już maszyny stanu, która może dodać sporo narzutu (przenoszenie lokalnych wartości do pól automatu stanowego, dodatkowe obiekty do GC).W takich przykładach, jeśli możesz tego uniknąć
async-await
i zwrócić oczekiwaną bezpośrednio, należy to zrobić, aby poprawić skuteczność metody.Zobacz to pytanie i tę odpowiedź, które są bardzo podobne do twojego pytania i tej odpowiedzi.
źródło