«Wait» fonctionne, mais l'appel de la tâche. Le résultat se bloque / se bloque

126

J'ai les quatre tests suivants et le dernier se bloque lorsque je l'exécute. Pourquoi cela arrive-t-il:

[Test]
public void CheckOnceResultTest()
{
    Assert.IsTrue(CheckStatus().Result);
}

[Test]
public async void CheckOnceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceAwaitTest()
{
    Assert.IsTrue(await CheckStatus());
    Assert.IsTrue(await CheckStatus());
}

[Test]
public async void CheckStatusTwiceResultTest()
{
    Assert.IsTrue(CheckStatus().Result); // This hangs
    Assert.IsTrue(await CheckStatus());
}

private async Task<bool> CheckStatus()
{
    var restClient = new RestClient(@"https://api.test.nordnet.se/next/1");
    Task<IRestResponse<DummyServiceStatus>> restResponse = restClient.ExecuteTaskAsync<DummyServiceStatus>(new RestRequest(Method.GET));
    IRestResponse<DummyServiceStatus> response = await restResponse;
    return response.Data.SystemRunning;
}

J'utilise cette méthode d'extension pour restsharp RestClient :

public static class RestClientExt
{
    public static Task<IRestResponse<T>> ExecuteTaskAsync<T>(this RestClient client, IRestRequest request) where T : new()
    {
        var tcs = new TaskCompletionSource<IRestResponse<T>>();
        RestRequestAsyncHandle asyncHandle = client.ExecuteAsync<T>(request, tcs.SetResult);
        return tcs.Task;
    }
}
public class DummyServiceStatus
{
    public string Message { get; set; }
    public bool ValidVersion { get; set; }
    public bool SystemRunning { get; set; }
    public bool SkipPhrase { get; set; }
    public long Timestamp { get; set; }
}

Pourquoi le dernier test se bloque-t-il?

Johan Larsson
la source
7
Vous devez éviter de renvoyer void des méthodes asynchrones. C'est uniquement pour la compatibilité descendante avec les gestionnaires d'événements existants, principalement dans le code d'interface. Si votre méthode asynchrone ne renvoie rien, elle doit renvoyer Task. J'ai eu de nombreux problèmes avec MSTest et annuler les tests asynchrones.
ghord
2
@ghord: MSTest ne prend pas du tout en charge async voidles méthodes de test unitaire; ils ne fonctionneront tout simplement pas. Cependant, NUnit le fait. Cela dit, je suis d' accord avec le principe général de préférer async Taskplus async void.
Stephen Cleary
@StephenCleary Oui, bien que cela soit autorisé dans les versions bêta de VS2012, ce qui causait toutes sortes de problèmes.
ghord

Réponses:

88

Vous vous heurtez à la situation de blocage standard que je décris sur mon blog et dans un article MSDN : la asyncméthode tente de planifier sa poursuite sur un thread bloqué par l'appel à Result.

Dans ce cas, votre SynchronizationContextest celui utilisé par NUnit pour exécuter async voidles méthodes de test. J'essaierais async Taskplutôt d' utiliser des méthodes de test.

Stephen Cleary
la source
4
le passage à la tâche asynchrone a fonctionné, je dois maintenant lire le contenu de vos liens plusieurs fois, monsieur.
Johan Larsson
@MarioLopez: La solution est d'utiliser " asynctout le chemin" (comme indiqué dans mon article MSDN). En d'autres termes - comme le titre de mon article de blog l'indique - "ne bloquez pas le code asynchrone".
Stephen Cleary
1
@StephenCleary et si je dois appeler une méthode asynchrone dans un constructeur? Les constructeurs ne peuvent pas être asynchrones.
Raikol Amaro
1
@StephenCleary Dans presque toutes vos réponses sur SO et dans vos articles, tout ce dont je vous vois parler est de remplacer Wait()par la méthode d'appel async. Mais pour moi, cela semble pousser le problème en amont. À un moment donné, quelque chose doit être géré de manière synchrone. Que faire si ma fonction est volontairement synchrone car elle gère des threads de travail de longue durée avec Task.Run()? Comment attendre que cela se termine sans interblocage dans mon test NUnit?
void.pointer
1
@ void.pointer: At some point, something has to be managed synchronously.- pas du tout. Pour les applications d'interface utilisateur, le point d'entrée peut être un async voidgestionnaire d'événements. Pour les applications serveur, le point d'entrée peut être une async Task<T>action. Il est préférable d'utiliser les asyncdeux pour éviter de bloquer les threads. Vous pouvez faire en sorte que votre test NUnit soit synchrone ou asynchrone; si asynchrone, faites-le async Taskau lieu de async void. S'il est synchrone, il ne devrait pas SynchronizationContexty avoir de blocage donc il ne devrait pas y avoir de blocage.
Stephen Cleary
223

Acquérir une valeur via une méthode asynchrone:

var result = Task.Run(() => asyncGetValue()).Result;

Appel synchrone d'une méthode asynchrone

Task.Run( () => asyncMethod()).Wait();

Aucun problème de blocage ne se produira en raison de l'utilisation de Task.Run.

Herman Schoenfeld
la source
15
-1 pour encourager l'utilisation des async voidméthodes de test unitaire et supprimer les garanties de même thread fournies par le SynchronizationContextdu système testé.
Stephen Cleary
68
@StephenCleary: il n'y a pas "d'enouraging" de vide asynchrone. Il utilise simplement une construction c # valide pour résoudre le problème de blocage. L'extrait ci-dessus est une solution de contournement indispensable et simple au problème du PO. Stackoverflow concerne les solutions aux problèmes, pas l'auto-promotion verbeuse.
Herman Schoenfeld
81
@StephenCleary: Vos articles n'articulent pas vraiment la solution (du moins pas clairement) et même si vous aviez une solution, vous utiliseriez de telles constructions indirectement. Ma solution n'utilise pas explicitement les contextes, et alors? Le fait est que la mienne fonctionne et que c'est une ligne unique. N'a pas besoin de deux articles de blog et de milliers de mots pour résoudre le problème. REMARQUE: je n'utilise même pas async void , donc je ne sais pas vraiment de quoi vous parlez. Voyez-vous "async void" quelque part dans ma réponse concise et correcte?
Herman Schoenfeld
15
@HermanSchoenfeld, si vous ajoutiez le pourquoi au comment , je pense que votre réponse en profiterait beaucoup.
ironstone13
19
Je sais que c'est un peu tard, mais vous devriez utiliser .GetAwaiter().GetResult()au lieu de .Resultpour qu'aucun Exceptionne soit emballé.
Camilo Terevinto du
15

Vous pouvez éviter un blocage en ajoutant ConfigureAwait(false)à cette ligne:

IRestResponse<DummyServiceStatus> response = await restResponse;

=>

IRestResponse<DummyServiceStatus> response = await restResponse.ConfigureAwait(false);

J'ai décrit cet écueil dans mon article de blog Pitfalls of async / await

Vladimir
la source
9

Vous bloquez l'interface utilisateur à l'aide de la propriété Task.Result. Dans la documentation MSDN, ils ont clairement mentionné que,

"La propriété Result est une propriété bloquante. Si vous essayez d'y accéder avant la fin de sa tâche, le thread actuellement actif est bloqué jusqu'à ce que la tâche se termine et que la valeur soit disponible. Dans la plupart des cas, vous devez accéder à la valeur en utilisant Attendre ou attendez au lieu d'accéder directement à la propriété. "

La meilleure solution pour ce scénario serait de supprimer à la fois wait et async des méthodes et d'utiliser uniquement la tâche où vous renvoyez le résultat. Cela ne gâchera pas votre séquence d'exécution.

Chevalier noir
la source
3

Si vous n'obtenez aucun rappel ou si le contrôle se bloque, après avoir appelé la fonction asynchrone du service / API, vous devez configurer Context pour renvoyer un résultat sur le même contexte appelé.

Utilisation TestAsync().ConfigureAwait(continueOnCapturedContext: false);

Vous serez confronté à ce problème uniquement dans les applications Web, mais pas dans static void main.

Mayank Pandit
la source
ConfigureAwaitévite les interblocages dans certains scénarios en ne s'exécutant pas dans le contexte de thread d'origine.
davidcarr