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

6.0

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();
}
5.0 6.0

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.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow