C# Language
Async-Await
Szukaj…
Wprowadzenie
W języku C # metoda zadeklarowana jako async
nie blokuje się w procesie synchronicznym, w przypadku korzystania z operacji opartych na operacjach we / wy (np. Dostęp do sieci, praca z plikami, ...). Na wynik takich metod oznaczonych asynchronicznie można oczekiwać za pomocą słowa kluczowego „ await
.
Uwagi
Metoda async
może zwrócić void
, Task
lub Task<T>
.
Typ zwracanego Task
będzie czekać na zakończenie metody, a wynik zostanie void
. Task<T>
zwróci wartość z typu T
po zakończeniu metody.
metody async
powinny zwracać Task
lub Task<T>
, w przeciwieństwie do void
, w prawie wszystkich okolicznościach. Nie można await
metod async void
, co prowadzi do różnych problemów. Jedynym scenariuszem, w którym async
powinna zwrócić wartość void
jest procedura obsługi zdarzenia.
async
/ await
działa poprzez przekształcenie metody async
w maszynę stanu. Robi to, tworząc za kulisami strukturę, która przechowuje bieżący stan i dowolny kontekst (np. Zmienne lokalne) i udostępnia MoveNext()
, aby przesuwać stany (i uruchamiać dowolny powiązany kod) za każdym razem, gdy oczekiwane oczekiwane zakończenie się zakończy.
Proste kolejne połączenia
public async Task<JobResult> GetDataFromWebAsync()
{
var nextJob = await _database.GetNextJobAsync();
var response = await _httpClient.GetAsync(nextJob.Uri);
var pageContents = await response.Content.ReadAsStringAsync();
return await _database.SaveJobResultAsync(pageContents);
}
Najważniejszą rzeczą do odnotowania tutaj jest to, że chociaż każda await
metoda jest wywoływana asynchronicznie - i na czas tego wywołania kontrola jest przekazywana z powrotem do systemu - przepływ wewnątrz metody jest liniowy i nie wymaga żadnego specjalnego traktowania ze względu na asynchronia. Jeśli którakolwiek z metod zwanych niepowodzeniem, wyjątek zostanie przetworzony „zgodnie z oczekiwaniami”, co w tym przypadku oznacza, że wykonanie metody zostanie przerwane, a wyjątek przejdzie na wyższy stos.
Spróbuj / Złap / Wreszcie
Począwszy od wersji C # 6.0, słowa kluczowego await
można teraz używać w ramach catch
i finally
zablokować.
try {
var client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
await client.LogExceptionAsync();
throw;
} finally {
await client.CloseAsync();
}
Przed wersją C # 6.0 musisz zrobić coś zgodnie z poniższymi instrukcjami. Zauważ, że 6.0 wyczyściło również kontrole zerowe za pomocą operatora Null Propagating .
AsynClient client;
MyException caughtException;
try {
client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
caughtException = ex;
}
if (client != null) {
if (caughtException != null) {
await client.LogExceptionAsync();
}
await client.CloseAsync();
if (caughtException != null) throw caughtException;
}
Należy pamiętać, że jeśli Task.Run
na zadanie, które nie zostało utworzone przez async
(np. Zadanie utworzone przez Task.Run
), niektóre debuggery mogą złamać wyjątki Task.Run
przez zadanie, nawet jeśli wydaje się, że jest obsługiwane przez otaczające try / catch. Dzieje się tak, ponieważ debugger uważa, że nie jest obsługiwany w odniesieniu do kodu użytkownika. W programie Visual Studio dostępna jest opcja „Just My Code” , którą można wyłączyć, aby zapobiec awariom debugera w takich sytuacjach.
Konfiguracja Web.config do targetowania 4.5 dla poprawnego zachowania asynchronicznego.
Środowisko web.config system.web.httpRuntime musi być celem 4.5, aby zapewnić, że wątek wypożyczy kontekst żądania przed wznowieniem metody asynchronicznej.
<httpRuntime targetFramework="4.5" />
Asynchronizacja i oczekiwanie mają niezdefiniowane zachowanie w ASP.NET przed wersją 4.5. Asynchronizacja / oczekiwanie zostanie wznowione w dowolnym wątku, który może nie mieć kontekstu żądania. Aplikacje pod obciążeniem losowo zakończą się niepowodzeniem, a wyjątki referencyjne o wartości zerowej uzyskują dostęp do HttpContext po oczekiwaniu. Korzystanie z HttpContext.Current w WebApi jest niebezpieczne z powodu asynchronizacji
Równoczesne połączenia
Możliwe jest jednoczesne oczekiwanie na wiele połączeń, najpierw wywołując oczekiwane zadania, a następnie oczekując na nie.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await firstTask;
await secondTask;
}
Alternatywnie, Task.WhenAll
można wykorzystać do grupowania wielu zadań w jedno Task
, które kończy się, gdy wszystkie jego przekazane zadania są zakończone.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await Task.WhenAll(firstTask, secondTask);
}
Możesz to również zrobić w pętli, na przykład:
List<Task> tasks = new List<Task>();
while (something) {
// do stuff
Task someAsyncTask = someAsyncMethod();
tasks.Add(someAsyncTask);
}
await Task.WhenAll(tasks);
Aby uzyskać wyniki z zadania po oczekiwaniu na wiele zadań w Task.WhenAll, po prostu zaczekaj na zadanie ponownie. Ponieważ zadanie jest już zakończone, po prostu zwróci wynik
var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();
await Task.WhenAll(task1, task2);
var result = await task2;
Ponadto Task.WhenAny
może służyć do równoległego wykonywania wielu zadań, takich jak Task.WhenAll
powyżej, z tą różnicą, że ta metoda zostanie zakończona, gdy którekolwiek z dostarczonych zadań zostanie ukończone.
public async Task RunConcurrentTasksWhenAny()
{
var firstTask = TaskOperation("#firstTask executed");
var secondTask = TaskOperation("#secondTask executed");
var thirdTask = TaskOperation("#thirdTask executed");
await Task.WhenAny(firstTask, secondTask, thirdTask);
}
Task
zwrócone przez RunConcurrentTasksWhenAny
zostanie zakończone po zakończeniu któregokolwiek z firstTask
, secondTask
lub thirdTask
.
Oczekuj operatora i słowa kluczowego asynchronicznego
await
operatora i async
słowo kluczowe spotykają się:
Metoda asynchroniczna, w której jest używane oczekiwanie, musi zostać zmodyfikowana słowem kluczowym async .
Przeciwnie, nie zawsze jest to prawda: możesz oznaczyć metodę jako async
bez użycia funkcji await
w jej ciele.
To, co faktycznie await
, to zawieszenie wykonania kodu do czasu zakończenia oczekiwanego zadania; każde zadanie może być oczekiwane.
Uwaga: nie można oczekiwać na metodę asynchroniczną, która nic nie zwraca (void).
W rzeczywistości słowo „zawiesza się” jest nieco mylące, ponieważ nie tylko wykonanie zatrzymuje się, ale wątek może stać się wolny do wykonywania innych operacji. Pod maską await
jest realizowane przez odrobinę magii kompilatora: dzieli metodę na dwie części - przed i po await
. Ta ostatnia część jest wykonywana po zakończeniu oczekiwanego zadania.
Jeśli zignorujemy niektóre ważne szczegóły, kompilator z grubsza zrobi to za Ciebie:
public async Task<TResult> DoIt()
{
// do something and acquire someTask of type Task<TSomeResult>
var awaitedResult = await someTask;
// ... do something more and produce result of type TResult
return result;
}
staje się:
public Task<TResult> DoIt()
{
// ...
return someTask.ContinueWith(task => {
var result = ((Task<TSomeResult>)task).Result;
return DoIt_Continuation(result);
});
}
private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
// ...
}
Każdą zwykłą metodę można przekształcić w asynchronię w następujący sposób:
await Task.Run(() => YourSyncMethod());
Może to być korzystne, gdy trzeba wykonać długo działającą metodę w wątku interfejsu użytkownika bez zamrażania interfejsu użytkownika.
Ale jest tu bardzo ważna uwaga: asynchroniczny nie zawsze oznacza współbieżny (równoległy lub nawet wielowątkowy). Nawet w jednym wątku async
- await
nadal pozwala na kod asynchroniczny. Na przykład zobacz ten niestandardowy harmonogram zadań . Taki „szalony” harmonogram zadań może po prostu przekształcić zadania w funkcje wywoływane w ramach przetwarzania pętli komunikatów.
Musimy zadać sobie pytanie: jaki wątek wykona kontynuację naszej metody DoIt_Continuation
?
Domyślnie operator await
planuje wykonanie kontynuacji z bieżącym kontekstem synchronizacji . Oznacza to, że domyślnie dla WinForms i WPF kontynuacja działa w wątku interfejsu użytkownika. Jeśli z jakiegoś powodu musisz zmienić to zachowanie, użyj metody Task.ConfigureAwait()
:
await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);
Zwracanie zadania bez oczekiwania
Metody, które wykonują operacje asynchroniczne nie trzeba używać await
jeżeli:
- W metodzie jest tylko jedno wywołanie asynchroniczne
- Wywołanie asynchroniczne znajduje się na końcu metody
- Wyjątek dotyczący łapania / obsługi, który może wystąpić w ramach zadania, nie jest konieczny
Rozważ tę metodę, która zwraca Task
:
public async Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return await dataStore.GetByKeyAsync(lookupKey);
}
Jeśli GetByKeyAsync
ma taki sam podpis jak GetUserAsync
(zwracający Task<User>
), metodę można uprościć:
public Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return dataStore.GetByKeyAsync(lookupKey);
}
W takim przypadku metoda nie musi być oznaczona jako async
, nawet jeśli wykonuje ona operację asynchroniczną. Zadanie zwrócone przez GetByKeyAsync
jest przekazywane bezpośrednio do metody wywołującej, gdzie będzie await
.
Ważne : Zwrócenie Task
zamiast czekania na niego zmienia zachowanie wyjątku metody, ponieważ nie spowoduje wyrzucenia wyjątku w metodzie, która uruchamia zadanie, ale w metodzie, która go oczekuje.
public Task SaveAsync()
{
try {
return dataStore.SaveChangesAsync();
}
catch(Exception ex)
{
// this will never be called
logger.LogException(ex);
}
}
// Some other code calling SaveAsync()
// If exception happens, it will be thrown here, not inside SaveAsync()
await SaveAsync();
Poprawi to wydajność, ponieważ zaoszczędzi kompilatorowi generowania dodatkowej asynchronicznej maszyny stanów.
Blokowanie kodu asynchronicznego może powodować zakleszczenia
Blokowanie połączeń asynchronicznych jest złą praktyką, ponieważ może powodować zakleszczenia w środowiskach, które mają kontekst synchronizacji. Najlepszą praktyką jest używanie asynchronizacji / czekania „do samego końca”. Na przykład następujący kod formularzy Windows Forms powoduje zakleszczenie:
private async Task<bool> TryThis()
{
Trace.TraceInformation("Starting TryThis");
await Task.Run(() =>
{
Trace.TraceInformation("In TryThis task");
for (int i = 0; i < 100; i++)
{
// This runs successfully - the loop runs to completion
Trace.TraceInformation("For loop " + i);
System.Threading.Thread.Sleep(10);
}
});
// This never happens due to the deadlock
Trace.TraceInformation("About to return");
return true;
}
// Button click event handler
private void button1_Click(object sender, EventArgs e)
{
// .Result causes this to block on the asynchronous call
bool result = TryThis().Result;
// Never actually gets here
Trace.TraceInformation("Done with result");
}
Zasadniczo po zakończeniu wywołania asynchronicznego czeka on na kontekst synchronizacji, który stanie się dostępny. Jednak procedura obsługi zdarzeń „trzyma się” kontekstu synchronizacji, czekając na zakończenie metody TryThis()
, co powoduje cykliczne oczekiwanie.
Aby to naprawić, należy zmodyfikować kod na
private async void button1_Click(object sender, EventArgs e)
{
bool result = await TryThis();
Trace.TraceInformation("Done with result");
}
Uwaga: procedury obsługi zdarzeń są jedynym miejscem, w którym należy użyć async void
(ponieważ nie można oczekiwać na metodę async void
).
Asynchronizacja / oczekiwanie poprawi wydajność tylko wtedy, gdy pozwoli maszynie wykonać dodatkową pracę
Rozważ następujący kod:
public async Task MethodA()
{
await MethodB();
// Do other work
}
public async Task MethodB()
{
await MethodC();
// Do other work
}
public async Task MethodC()
{
// Or await some other async work
await Task.Delay(100);
}
To nie będzie działać lepiej niż
public void MethodA()
{
MethodB();
// Do other work
}
public void MethodB()
{
MethodC();
// Do other work
}
public void MethodC()
{
Thread.Sleep(100);
}
Podstawowym celem asynchronizacji / oczekiwania jest umożliwienie maszynie wykonania dodatkowej pracy - na przykład umożliwienie wątkowi wywołującemu wykonanie innej pracy, gdy czeka on na wynik jakiejś operacji we / wy. W takim przypadku wątek wywołujący nigdy nie może wykonywać więcej pracy niż byłby w stanie wykonać w inny sposób, więc nie ma przyrostu wydajności po prostu wywołując MethodA()
, MethodB()
i MethodC()
synchronicznie.