web-dev-qa-db-fra.com

Appel de méthodes asynchrones à partir de code non asynchrone

Je suis en train de mettre à jour une bibliothèque avec une surface d'API construite dans .NET 3.5. En conséquence, toutes les méthodes sont synchrones. Je ne peux pas changer l’API (c’est-à-dire convertir les valeurs de retour en tâche), car cela impliquerait que tous les appelants changent. Il ne me reste donc plus qu'à appeler les méthodes asynchrones de manière synchrone. Ceci est dans le contexte des applications de console ASP.NET 4, ASP.NET Core et .NET/.NET Core.

Je n'ai peut-être pas été assez clair - la situation est que mon code existant est non asynchrone et que je souhaite utiliser de nouvelles bibliothèques telles que System.Net.Http et le kit SDK AWS qui prennent en charge uniquement les méthodes asynchrones. J'ai donc besoin de combler le fossé et de pouvoir disposer d'un code pouvant être appelé de manière synchrone, mais pouvant ensuite appeler des méthodes asynchrones ailleurs.

J'ai beaucoup lu et il y a eu plusieurs fois des réponses à cette question.

appel d'une méthode asynchrone à partir d'une méthode non asynchrone

En attente synchrone pour une opération async, et pourquoi Wait () gèle-t-il le programme ici

Appel d'une méthode asynchrone à partir d'une méthode synchrone

Comment pourrais-je exécuter une méthode Async Task <T> de manière synchrone?

Appel de la méthode asynchrone de manière synchrone

Comment appeler une méthode asynchrone à partir d'une méthode synchrone en C #?

Le problème est que la plupart des réponses sont différentes! L'approche la plus courante que j'ai vue est d'utiliser .Result, mais cela peut aboutir à une impasse. J'ai essayé toutes les solutions suivantes, et elles fonctionnent, mais je ne sais pas quelle est la meilleure approche pour éviter les blocages, obtenir de bonnes performances et jouer avec le temps d'exécution (en termes d'honorer les planificateurs de tâches, les options de création de tâches, etc. ) Y a-t-il une réponse définitive? Quelle est la meilleure approche?

private static T taskSyncRunner<T>(Func<Task<T>> task)
    {
        T result;
        // approach 1
        result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 2
        result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 3
        result = task().ConfigureAwait(false).GetAwaiter().GetResult();

        // approach 4
        result = Task.Run(task).Result;

        // approach 5
        result = Task.Run(task).GetAwaiter().GetResult();


        // approach 6
        var t = task();
        t.RunSynchronously();
        result = t.Result;

        // approach 7
        var t1 = task();
        Task.WaitAll(t1);
        result = t1.Result;

        // approach 8?

        return result;
    }
58
Erick T

Il ne me reste donc plus qu'à appeler les méthodes asynchrones de manière synchrone.

Tout d’abord, c’est une bonne chose à faire. Je dis cela parce qu'il est courant sur Stack Overflow de le signaler comme un acte du diable comme une déclaration générale sans tenir compte du cas concret.

Il n'est pas nécessaire d'être asynchrone jusqu'au bout pour être correct. Bloquer un élément asynchrone pour le synchroniser a un coût de performance qui peut avoir une importance ou peut être totalement hors de propos. Cela dépend du cas concret.

Les deadlocks proviennent de deux threads qui tentent d'entrer le même contexte de synchronisation mono-thread en même temps. Toute technique qui évite cela évite de manière fiable les blocages causés par le blocage.

Ici, tous vos appels à .ConfigureAwait(false) sont inutiles car vous n’attendez pas.

RunSynchronously n'est pas valide à utiliser car toutes les tâches ne peuvent pas être traitées de cette façon.

.GetAwaiter().GetResult() diffère de Result/Wait() en ce qu'il imite le comportement de propagation de l'exception await. Vous devez décider si vous le souhaitez ou non. (Recherchez donc quel est ce comportement; inutile de le répéter ici.)

En plus de cela, toutes ces approches ont des performances similaires. Ils allouent un événement du système d'exploitation d'une manière ou d'une autre et le bloquent. C'est la partie chère. Je ne sais pas quelle approche est absolument la moins chère.

Personnellement, j'aime bien le modèle Task.Run(() => DoSomethingAsync()).Wait(); car il évite catégoriquement les blocages, est simple et ne cache pas certaines exceptions que GetResult() pourrait masquer. Mais vous pouvez aussi utiliser GetResult() avec cela.

72
usr

Je suis en train de mettre à jour une bibliothèque avec une surface d'API construite dans .NET 3.5. En conséquence, toutes les méthodes sont synchrones. Je ne peux pas changer l’API (c’est-à-dire convertir les valeurs de retour en tâche), car cela impliquerait que tous les appelants changent. Il ne me reste donc plus qu'à appeler les méthodes asynchrones de manière synchrone.

Il n'y a pas de "meilleur" moyen universel d'effectuer l'anti-motif sync-async. Seules une variété de piratages ayant chacun leurs inconvénients.

Ce que je recommande, c'est de conserver les anciennes API synchrones, puis de les introduire à leurs côtés. Vous pouvez faire cela en utilisant le "hack d'argument booléen" comme décrit dans mon article MSDN sur Brownfield Async .

Tout d’abord, une brève explication des problèmes posés par chaque approche dans votre exemple:

  1. ConfigureAwait n'a de sens que lorsqu'il existe un await; sinon, ça ne fait rien.
  2. Result encapsulera les exceptions dans un AggregateException; si vous devez bloquer, utilisez plutôt GetAwaiter().GetResult().
  3. Task.Run exécutera son code sur un thread de pool de threads (évidemment). Ceci est correct uniquement si le code peut être exécuté sur un thread de pool de threads.
  4. RunSynchronously est une API avancée utilisée dans des situations extrêmement rares lors du parallélisme dynamique basé sur des tâches. Vous n'êtes pas du tout dans ce scénario.
  5. Task.WaitAll avec une tâche unique est identique à Wait().
  6. async () => await x n'est qu'un moyen moins efficace de dire () => x.
  7. Blocage d'une tâche démarrée à partir du thread actuel peut provoquer des blocages .

Voici la ventilation:

// Problems (1), (3), (6)
result = Task.Run(async () => await task()).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (3)
result = Task.Run(task).ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (1), (7)
result = task().ConfigureAwait(false).GetAwaiter().GetResult();

// Problems (2), (3)
result = Task.Run(task).Result;

// Problems (3)
result = Task.Run(task).GetAwaiter().GetResult();

// Problems (2), (4)
var t = task();
t.RunSynchronously();
result = t.Result;

// Problems (2), (5)
var t1 = task();
Task.WaitAll(t1);
result = t1.Result;

Au lieu de n’importe laquelle de ces approches, puisque vous avez du code synchrone actif existant, vous devez l’utiliser parallèlement au code le plus récent, naturellement asynchrone. Par exemple, si votre code existant a utilisé WebClient:

public string Get()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

et vous voulez ajouter une API asynchrone, alors je le ferais comme ceci:

private async Task<string> GetCoreAsync(bool sync)
{
  using (var client = new WebClient())
  {
    return sync ?
        client.DownloadString(...) :
        await client.DownloadStringTaskAsync(...);
  }
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

ou, si vous devez utiliser HttpClient pour une raison quelconque:

private string GetCoreSync()
{
  using (var client = new WebClient())
    return client.DownloadString(...);
}

private static HttpClient HttpClient { get; } = ...;

private async Task<string> GetCoreAsync(bool sync)
{
  return sync ?
      GetCoreSync() :
      await HttpClient.GetString(...);
}

public string Get() => GetCoreAsync(sync: true).GetAwaiter().GetResult();

public Task<string> GetAsync() => GetCoreAsync(sync: false);

Avec cette approche, votre logique irait dans les méthodes Core, qui peuvent être exécutées de manière synchrone ou asynchrone (comme déterminé par le paramètre sync.). Si sync est true, les méthodes de base doivent renvoyer une tâche déjà terminée. Pour l'implémentation, utilisez des API synchrones pour une exécution synchrone, et des API asynchrones pour une exécution asynchrone.

Finalement, je recommande de déconseiller les API synchrones.

31
Stephen Cleary