Recherche…


Introduction

En C #, une méthode déclarée async ne sera pas bloquée dans un processus synchrone, dans le cas où vous utiliseriez des opérations basées sur les E / S (par exemple, accès Web, utilisation de fichiers, ...). Le résultat de telles méthodes marquées par async peut être attendu via l'utilisation du mot-clé await .

Remarques

Une méthode async peut renvoyer void , Task ou Task<T> .

Le type de retour Task attendra la fin de la méthode et le résultat sera void . Task<T> renverra une valeur du type T fois la méthode terminée.

async méthodes async doivent renvoyer Task ou Task<T> , par opposition à void , dans presque toutes les circonstances. async void méthodes async void ne peuvent pas être await , ce qui entraîne divers problèmes. Le seul scénario où un async doit retourner void est dans le cas d'un gestionnaire d'événements.

async / await fonctionne en transformant votre méthode async en une machine à états. Pour ce faire, il crée une structure en arrière-plan qui stocke l'état actuel et tout contexte (comme les variables locales) et expose une méthode MoveNext() pour avancer les états (et exécuter tout code associé) chaque fois qu'une attente attendue se termine.

Appels consécutifs simples

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 principale chose à noter ici est que si tous les await méthode -ed est appelé de manière asynchrone - et pour le moment de cet appel le contrôle est donné au système - l'écoulement à l' intérieur de la méthode est linéaire et ne nécessite aucun traitement spécial en raison de asynchronie. Si l'une des méthodes appelées échoue, l'exception sera traitée "comme prévu", ce qui dans ce cas signifie que l'exécution de la méthode sera abandonnée et que l'exception passera à la pile.

Try / Catch / Finalement

6,0

A partir de C # 6.0, le mot-clé await peut maintenant être utilisé dans un bloc catch et finally .

try {
   var client = new AsyncClient();
   await client.DoSomething();
} catch (MyException ex) {
   await client.LogExceptionAsync();
   throw;
} finally {
   await client.CloseAsync();
}
5,0 6,0

Avant C # 6.0, vous deviez faire quelque chose comme suit. Notez que 6.0 a également nettoyé les vérifications NULL avec l' opérateur 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;
}

Notez que si vous attendez une tâche non créée par async (par exemple, une tâche créée par Task.Run ), certains débogueurs peuvent casser des exceptions lancées par la tâche, même si elle est apparemment gérée par la méthode try / catch environnante. Cela se produit car le débogueur considère qu'il n'est pas géré par rapport au code utilisateur. Dans Visual Studio, il existe une option appelée "Just My Code" , qui peut être désactivée pour empêcher le débogueur de se briser dans de telles situations.

Configuration de Web.config sur la cible 4.5 pour un comportement asynchrone correct.

Web.config system.web.httpRuntime doit cibler la version 4.5 pour garantir que le thread loue le contexte de la requête avant de reprendre votre méthode asynchrone.

<httpRuntime targetFramework="4.5" />

Async et wait ont un comportement indéfini sur ASP.NET avant 4.5. L'async / wait reprendra sur un thread arbitraire qui pourrait ne pas avoir le contexte de la requête. Les applications sous charge échoueront aléatoirement avec des exceptions de référence NULL accédant au HttpContext après l'attente. Utiliser HttpContext.Current dans WebApi est dangereux à cause de l’async

Appels simultanés

Il est possible d'attendre plusieurs appels en même temps qu'en invoquant d' abord les tâches awaitable et qui les attend.

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

    await firstTask;
    await secondTask;
}

Vous pouvez également utiliser Task.WhenAll pour regrouper plusieurs tâches en une seule Task , qui se termine lorsque toutes les tâches réussies sont terminées.

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

    await Task.WhenAll(firstTask, secondTask);
}

Vous pouvez également faire cela dans une boucle, par exemple:

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

await Task.WhenAll(tasks);

Pour obtenir les résultats d'une tâche après avoir attendu plusieurs tâches avec Task.WhenAll, attendez simplement la tâche à nouveau. Comme la tâche est déjà terminée, le résultat sera renvoyé

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

await Task.WhenAll(task1, task2);

var result = await task2;

En outre, Task.WhenAny peut être utilisé pour exécuter plusieurs tâches en parallèle, telles que Task.WhenAll ci-dessus, à la différence que cette méthode se terminera lorsque l' une des tâches fournies sera terminée.

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

La Task renvoyée par RunConcurrentTasksWhenAny se terminera à la fin de firstTask , secondTask ou thirdTask .

Attendez l'opérateur et le mot-clé asynchrone

await opérateur et le mot-clé async réunis:

La méthode asynchrone dans laquelle wait est utilisée doit être modifiée par le mot-clé async .

Le contraire n'est pas toujours vrai: vous pouvez marquer une méthode comme async sans utiliser await dans son corps.

Ce qui await réalité, c'est de suspendre l'exécution du code jusqu'à la fin de la tâche attendue; toute tâche peut être attendue.

Remarque: vous ne pouvez pas attendre la méthode asynchrone qui ne retourne rien (void).

En fait, le mot «suspend» est un peu trompeur car non seulement l'exécution s'arrête, mais le thread peut devenir libre pour exécuter d'autres opérations. Sous le capot, await est implémenté par un peu de magie du compilateur: il divise une méthode en deux parties - avant et après l' await . La dernière partie est exécutée à la fin de la tâche attendue.

Si nous ignorons certains détails importants, le compilateur le fait grosso modo pour vous:

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

devient:

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

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

Toute méthode habituelle peut être transformée en asynchrone de la manière suivante:

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

Cela peut être avantageux lorsque vous devez exécuter une méthode longue sur le thread d'interface utilisateur sans bloquer l'interface utilisateur.

Mais il y a une remarque très importante ici: Asynchrone ne signifie pas toujours concurrente (parallèle ou même multithread). Même sur un seul thread, async - await permet toujours le code asynchrone. Par exemple, consultez ce planificateur de tâches personnalisé. Un tel planificateur de tâches «fou» peut simplement transformer des tâches en fonctions appelées dans le traitement de la boucle de messages.

Nous devons nous demander: quel thread exécutera la suite de notre méthode DoIt_Continuation ?

Par défaut, l'opérateur d' await planifie l'exécution de la continuation avec le contexte de synchronisation actuel . Cela signifie que par défaut pour WinForms et la continuation WPF s'exécute dans le thread d'interface utilisateur. Si, pour une raison quelconque, vous devez modifier ce comportement, utilisez la méthode Task.ConfigureAwait() :

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

Retourner une tâche sans attendre

Les méthodes qui effectuent des opérations asynchrones ne ont pas besoin d'utiliser await si:

  • Il n'y a qu'un seul appel asynchrone dans la méthode
  • L'appel asynchrone est à la fin de la méthode
  • Une exception de capture / traitement pouvant survenir dans la tâche n'est pas nécessaire

Considérez cette méthode qui renvoie une Task :

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

    return await dataStore.GetByKeyAsync(lookupKey);
}

Si GetByKeyAsync a la même signature que GetUserAsync (en retournant une Task<User> ), la méthode peut être simplifiée:

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

    return dataStore.GetByKeyAsync(lookupKey);
}

Dans ce cas, la méthode n'a pas besoin d'être marquée async , même si elle effectue une opération asynchrone. Le Groupe retourné par GetByKeyAsync est transmis directement à la méthode d'appel, où il sera await ed.

Important : Le fait de renvoyer la Task au lieu de l'attendre modifie le comportement d'exception de la méthode, car elle ne déclenche pas l'exception dans la méthode qui lance la tâche mais dans la méthode qui l'attend.

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

Cela améliorera les performances car cela permettra au compilateur d'économiser la génération d'une machine à états asynchrone supplémentaire.

Le blocage sur du code asynchrone peut provoquer des blocages

Il est déconseillé de bloquer les appels asynchrones car cela peut provoquer des blocages dans des environnements dotés d'un contexte de synchronisation. La meilleure pratique consiste à utiliser Async / Wait "tout en bas". Par exemple, le code Windows Forms suivant provoque un blocage:

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

Essentiellement, une fois l'appel asynchrone terminé, il attend que le contexte de synchronisation devienne disponible. Cependant, le gestionnaire d'événement "conserve" le contexte de synchronisation pendant qu'il attend que la méthode TryThis() se termine, provoquant ainsi une attente circulaire.

Pour corriger cela, le code doit être modifié pour

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

Remarque: les gestionnaires d'événements sont le seul endroit où async void doit être utilisé (car vous ne pouvez pas attendre une méthode async void ).

Async / wait n'améliorera les performances que si elle permet à la machine d'effectuer des tâches supplémentaires

Considérez le code suivant:

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

Cela ne fonctionnera pas mieux que

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

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

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

Le but principal de async / waiting est de permettre à la machine d'effectuer un travail supplémentaire, par exemple pour permettre au thread appelant d'effectuer d'autres tâches en attendant le résultat d'une opération d'E / S. Dans ce cas, le thread appelant n'est jamais autorisé à faire plus de travail que ce qu'il aurait pu faire autrement, il n'y a donc pas de gain de performance par rapport à l'appel MethodA() , MethodB() et MethodC() .



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow