Ricerca…


introduzione

In C #, un metodo dichiarato async non bloccherà all'interno di un processo sincrono, nel caso in cui si stiano utilizzando operazioni basate sull'I / O (ad esempio accesso Web, utilizzo di file, ...). Il risultato di tali metodi asincroni contrassegnati può essere atteso tramite l'uso della parola chiave await .

Osservazioni

Un metodo async può restituire void , Task o Task<T> .

Il tipo di ritorno Task attenderà che il metodo finisca e il risultato sarà void . Task<T> restituirà un valore da tipo T dopo il completamento del metodo.

async metodi async dovrebbero restituire Task o Task<T> , in contrasto con il void , in quasi tutte le circostanze. non è possibile await metodi async void , il che porta a una serie di problemi. L'unico scenario in cui un async deve restituire void è nel caso di un gestore di eventi.

async / await funziona trasformando il tuo metodo async in una macchina a stati. Lo fa creando una struttura dietro le quinte che memorizza lo stato corrente e qualsiasi contesto (come le variabili locali) e espone un metodo MoveNext() per far avanzare gli stati (ed eseguire qualsiasi codice associato) ogni volta che un attendibile atteso si completa.

Semplici chiamate consecutive

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);
}

La cosa principale da notare qui è che mentre ogni await metodo -ed è chiamato in modo asincrono - e per il tempo di quella chiamata il controllo viene ceduto al sistema - il flusso all'interno del metodo è lineare e non richiede alcun trattamento speciale a causa di asincronia. Se uno dei metodi chiamati fallisce, l'eccezione verrà elaborata "come previsto", che in questo caso significa che l'esecuzione del metodo verrà interrotta e l'eccezione salirà nello stack.

Try / catch / finally

6.0

A partire da C # 6.0, la parola chiave await ora può essere utilizzata all'interno di un blocco catch e finally .

try {
   var client = new AsyncClient();
   await client.DoSomething();
} catch (MyException ex) {
   await client.LogExceptionAsync();
   throw;
} finally {
   await client.CloseAsync();
}
5.0 6.0

Prima di C # 6.0, avresti bisogno di fare qualcosa sulla falsariga di quanto segue. Si noti che 6.0 ha anche ripulito i controlli null con l' operatore 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;
}

Si noti che se si attende un'attività non creata da async (ad esempio un'attività creata da Task.Run ), alcuni debugger potrebbero interrompersi sulle eccezioni generate dall'attività anche se apparentemente gestite dal try / catch circostante. Questo accade perché il debugger lo considera non gestito rispetto al codice utente. In Visual Studio, c'è un'opzione chiamata "Just My Code" , che può essere disattivata per impedire che il debugger si rompa in tali situazioni.

Installazione di Web.config su target 4.5 per il corretto comportamento asincrono.

Il web.config system.web.httpRuntime deve targetizzare 4.5 per garantire che il thread affitterà il contesto della richiesta prima di riprendere il metodo asincrono.

<httpRuntime targetFramework="4.5" />

Async e attendere hanno un comportamento indefinito su ASP.NET precedente alla 4.5. Async / await riprenderà su un thread arbitrario che potrebbe non avere il contesto della richiesta. Le applicazioni sotto carico falliranno casualmente con eccezioni di riferimento null accedendo a HttpContext dopo l'attesa. L'utilizzo di HttpContext.Current in WebApi è pericoloso a causa di async

Chiamate contemporanee

È possibile attendere più chiamate contemporaneamente, invocando dapprima le attività attendibili e quindi aspettandole.

public async Task RunConcurrentTasks()
{
    var firstTask = DoSomethingAsync();
    var secondTask = DoSomethingElseAsync();

    await firstTask;
    await secondTask;
}

In alternativa, Task.WhenAll può essere utilizzato per raggruppare più attività in una singola Task , che viene completata quando tutte le attività passate sono complete.

public async Task RunConcurrentTasks()
{
    var firstTask = DoSomethingAsync();
    var secondTask = DoSomethingElseAsync();

    await Task.WhenAll(firstTask, secondTask);
}

Puoi anche farlo all'interno di un ciclo, ad esempio:

List<Task> tasks = new List<Task>();
while (something) {
    // do stuff
    Task someAsyncTask = someAsyncMethod();
    tasks.Add(someAsyncTask);
}

await Task.WhenAll(tasks);

Per ottenere risultati da un'attività dopo aver atteso più attività con Task.WhenAll, è sufficiente attendere di nuovo l'attività. Poiché l'attività è già completata, verrà restituito il risultato

var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();

await Task.WhenAll(task1, task2);

var result = await task2;

Inoltre, Task.WhenAny può essere utilizzato per eseguire più attività in parallelo, come Task.WhenAll sopra, con la differenza che questo metodo verrà completato quando verrà completata una delle attività fornite.

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);
}

L' Task restituita da RunConcurrentTasksWhenAny verrà completata quando una delle operazioni firstTask , secondTask o thirdTask completata.

Attendere l'operatore e la parola chiave asincrona

await operatore e la parola chiave async si uniscano:

Il metodo asincrono in cui viene utilizzata l' attesa deve essere modificato dalla parola chiave async .

L'opposto non è sempre vero: puoi contrassegnare un metodo come async senza utilizzare l' await nel suo corpo.

Ciò che in realtà await è sospendere l'esecuzione del codice fino al completamento dell'attività atteso; qualsiasi attività può essere attesa.

Nota: non è possibile attendere il metodo asincrono che non restituisce nulla (nulla).

In realtà, la parola 'suspends' è un po 'fuorviante perché non solo l'esecuzione si arresta, ma il thread può diventare libero per l'esecuzione di altre operazioni. Sotto il cofano, l' await è implementata da un po 'di magia del compilatore: divide un metodo in due parti - prima e dopo l' await . L'ultima parte viene eseguita quando l'attività attesa viene completata.

Se ignoriamo alcuni dettagli importanti, il compilatore lo fa grosso modo per te:

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;
}

diventa:

public Task<TResult> DoIt()
{
    // ...
    return someTask.ContinueWith(task => {
        var result = ((Task<TSomeResult>)task).Result;
        return DoIt_Continuation(result);
    });
}

private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
    // ...
}

Qualsiasi metodo usuale può essere trasformato in asincrono nel seguente modo:

await Task.Run(() => YourSyncMethod());

Ciò può essere vantaggioso quando è necessario eseguire un metodo a esecuzione prolungata sul thread dell'interfaccia utente senza bloccare l'interfaccia utente.

Ma c'è un'osservazione molto importante qui: Asincrono non significa sempre concomitante (parallelo o addirittura multi-thread). Anche su un singolo thread, async - await consente ancora il codice asincrono. Ad esempio, vedere questo Utilità di pianificazione personalizzata. Un programmatore di compiti così "pazzo" può semplicemente trasformare compiti in funzioni chiamate all'interno dell'elaborazione del loop di messaggi.

Dobbiamo chiederci: quale thread eseguirà la continuazione del nostro metodo DoIt_Continuation ?

Per impostazione predefinita, l'operatore di await pianifica l'esecuzione della continuazione con il contesto di sincronizzazione corrente. Significa che per impostazione predefinita per le sequenze di esecuzione WinForms e WPF nel thread dell'interfaccia utente. Se, per qualche motivo, è necessario modificare questo comportamento, utilizzare il metodo Task.ConfigureAwait() :

await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);

Restituzione di un'attività senza attendere

I metodi che eseguono operazioni asincrone non devono essere utilizzati await se:

  • C'è solo una chiamata asincrona all'interno del metodo
  • La chiamata asincrona è alla fine del metodo
  • L'eccezione di cattura / gestione che può verificarsi all'interno dell'attività non è necessaria

Considerate questo metodo che restituisce un Task :

public async Task<User> GetUserAsync(int id)
{
    var lookupKey = "Users" + id;

    return await dataStore.GetByKeyAsync(lookupKey);
}

Se GetByKeyAsync ha la stessa firma di GetUserAsync (restituendo un'attività Task<User> ), il metodo può essere semplificato:

public Task<User> GetUserAsync(int id)
{
    var lookupKey = "Users" + id;

    return dataStore.GetByKeyAsync(lookupKey);
}

In questo caso, il metodo non deve essere contrassegnato come async , anche se sta preformando un'operazione asincrona. L'attività restituita da GetByKeyAsync viene passata direttamente al metodo chiamante, dove sarà await ed.

Importante : se si restituisce l' Task invece di attenderla, cambia il comportamento dell'eccezione del metodo, poiché non getterà l'eccezione all'interno del metodo che avvia l'attività ma nel metodo che la attende.

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();

Ciò migliorerà le prestazioni poiché salverà il compilatore la generazione di una macchina di stato asincrono extra.

Il blocco del codice asincrono può causare deadlock

È una cattiva pratica bloccare le chiamate asincrone poiché può causare deadlock in ambienti con un contesto di sincronizzazione. La migliore pratica è usare async / attendere "fino in fondo". Ad esempio, il seguente codice Windows Form causa un deadlock:

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");
}

In sostanza, una volta completata la chiamata asincrona, attende che il contesto di sincronizzazione sia disponibile. Tuttavia, il gestore eventi "trattiene" il contesto di sincronizzazione mentre è in attesa del TryThis() metodo TryThis() , causando quindi un'attesa circolare.

Per risolvere questo problema, il codice dovrebbe essere modificato in

private async void button1_Click(object sender, EventArgs e)
{
  bool result = await TryThis();
  Trace.TraceInformation("Done with result");
}

Nota: i gestori di eventi sono l'unico posto in cui dovrebbe essere usato il async void (perché non è possibile attendere un metodo di async void ).

Async / await migliorerà le prestazioni solo se consente alla macchina di eseguire ulteriori operazioni

Considera il seguente codice:

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);
}

Questo non funzionerà meglio di

public void MethodA()
{
     MethodB();
     // Do other work
}

public void MethodB()
{
     MethodC();
     // Do other work
}

public void MethodC()
{
     Thread.Sleep(100);
}

Lo scopo principale di async / await è di consentire alla macchina di eseguire operazioni aggiuntive, ad esempio per consentire al thread chiamante di eseguire altri lavori mentre è in attesa di un risultato da qualche operazione di I / O. In questo caso, il thread chiamante non ha mai il permesso di fare più lavoro di quanto sarebbe stato in grado di fare altrimenti, quindi non ci sono guadagni di prestazioni semplicemente chiamando MethodA() , MethodB() e MethodC() sincrono.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow