114 votes

Pourquoi cette action asynchrone se bloque-t-elle lorsque j'essaie d'accéder à la propriété Result de ma tâche ?

J'ai une application .Net 4.5 multi-niveaux qui appelle une méthode utilisant le nouvel outil C# async y await des mots-clés qui se bloquent et je ne vois pas pourquoi.

En bas, j'ai une méthode asynchrone qui étend notre utilitaire de base de données. OurDBConn (il s'agit essentiellement d'une enveloppe pour le système sous-jacent DBConnection y DBCommand objets) :

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

Ensuite, j'ai une méthode asynchrone de niveau intermédiaire qui appelle cette méthode pour obtenir des totaux lents :

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

Enfin, j'ai une méthode UI (une action MVC) qui s'exécute de manière synchrone :

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

Le problème est qu'il s'accroche à cette dernière ligne pour toujours. Il fait la même chose si j'appelle asyncTask.Wait() . Si j'exécute directement la méthode SQL lente, cela prend environ 4 secondes.

Le comportement que j'attends est que lorsqu'il arrive à asyncTask.Result si elle n'est pas terminée, elle doit attendre qu'elle le soit, et une fois qu'elle l'est, elle doit renvoyer le résultat.

Si j'interviens avec un débogueur, l'instruction SQL se termine et la fonction lambda se termine, mais la fonction return result; ligne de GetTotalAsync n'est jamais atteint.

Une idée de ce que je fais mal ?

Avez-vous des suggestions sur la façon dont je dois enquêter pour résoudre ce problème ?

Pourrait-il s'agir d'un blocage quelque part, et si oui, existe-t-il un moyen direct de le trouver ?

162voto

Jason Malinowski Points 6493

Oui, c'est bien une impasse. Et une erreur courante avec le TPL, alors ne vous sentez pas mal.

Quand vous écrivez await foo le runtime, par défaut, planifie la continuation de la fonction sur le même SynchronizationContext que celui sur lequel la méthode a démarré. En anglais, disons que vous avez appelé votre méthode ExecuteAsync depuis le fil d'exécution de l'interface utilisateur. Votre requête s'exécute sur le thread du pool de threads (parce que vous avez appelé Task.Run ), mais vous attendez ensuite le résultat. Cela signifie que le runtime va programmer votre " return result; "pour qu'elle soit exécutée sur le thread de l'interface utilisateur, plutôt que de la planifier dans le pool de threads.

Alors comment cette impasse ? Imaginez que vous avez juste ce code :

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

La première ligne donne donc le coup d'envoi du travail asynchrone. La deuxième ligne bloque le thread de l'interface utilisateur . Ainsi, lorsque le runtime veut exécuter la ligne "return result" sur le thread de l'interface utilisateur, il ne peut pas le faire tant que la ligne "return result" n'a pas été exécutée. Result complète. Mais bien sûr, le résultat ne peut pas être donné avant que le retour ne se produise. Impasse.

Ceci illustre une règle clé de l'utilisation de la TPL : lorsque vous utilisez .Result sur un thread d'interface utilisateur (ou tout autre contexte de synchronisation), vous devez veiller à ce que rien de ce dont la tâche dépend ne soit programmé sur le thread d'interface utilisateur. Sinon, des malheurs se produiront.

Alors, que faites-vous ? La première option est d'utiliser await partout, mais comme vous l'avez dit, ce n'est déjà pas une option. La deuxième option qui s'offre à vous est de simplement arrêter d'utiliser await. Vous pouvez réécrire vos deux fonctions en :

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

Quelle est la différence ? Il n'y a plus d'attente nulle part, donc rien n'est implicitement programmé dans le thread de l'interface utilisateur. Pour les méthodes simples comme celles-ci qui n'ont qu'un seul retour, il n'y a aucun intérêt à faire un " var result = await...; return result "Il suffit d'enlever le modificateur asynchrone et de passer directement l'objet tâche. C'est moins de frais généraux, si ce n'est plus.

L'option n°3 consiste à spécifier que vous ne voulez pas que vos attentes soient replanifiées sur le thread de l'interface utilisateur, mais simplement sur le pool de threads. Pour ce faire, utilisez l'option ConfigureAwait comme suit :

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

L'attente d'une tâche est normalement programmée sur le thread de l'interface utilisateur si vous êtes dessus ; l'attente du résultat d'une tâche de l'interface utilisateur est programmée sur le thread de l'interface utilisateur si vous êtes dessus. ContinueAwait ignorera le contexte dans lequel vous vous trouvez, et planifiera toujours le threadpool. L'inconvénient de cette méthode est que vous devez saupoudrer cette fonction partout dans toutes les fonctions dont dépend votre .Result, parce que toute fonction manquée .ConfigureAwait pourrait être la cause d'un autre blocage.

40voto

Stephen Cleary Points 91731

C'est le classique mélange async scénario de blocage, comme je le décris sur mon blog . Jason l'a bien décrit : par défaut, un "contexte" est sauvegardé à chaque await et utilisé pour poursuivre le async méthode. Ce "contexte" est l'actuel SynchronizationContext à moins qu'il ne le fasse null dans ce cas, il s'agit du courant TaskScheduler . Lorsque le async tente de poursuivre, elle réintègre d'abord le "contexte" capturé (dans ce cas, un fichier ASP.NET SynchronizationContext ). L'ASP.NET SynchronizationContext ne permet qu'un seul thread dans le contexte à la fois, et il y a déjà un thread dans le contexte - le thread bloqué sur Task.Result .

Il existe deux lignes directrices qui permettront d'éviter cette impasse :

  1. Utilice async jusqu'en bas. Vous dites que vous ne pouvez pas le faire, mais je ne vois pas pourquoi. ASP.NET MVC sur .NET 4.5 peut certainement prendre en charge les éléments suivants async et ce n'est pas un changement difficile à faire.
  2. Utilice ConfigureAwait(continueOnCapturedContext: false) autant que possible. Cela remplace le comportement par défaut qui consiste à reprendre sur le contexte capturé.

16voto

Danilow Points 150

J'étais dans la même situation de blocage mais dans mon cas, en appelant une méthode asynchrone à partir d'une méthode synchrone, ce qui a fonctionné pour moi était :

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

Est-ce une bonne approche, une idée ?

4voto

Cameron Jeffers Points 81

Juste pour ajouter à la réponse acceptée (pas assez de représentants pour commenter), j'ai rencontré ce problème en bloquant l'utilisation de l'option task.Result bien que chaque await en dessous, il avait ConfigureAwait(false) comme dans cet exemple :

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Le problème se situe en fait au niveau du code de la bibliothèque externe. La méthode asynchrone de la bibliothèque essayait de continuer dans le contexte synchrone appelant, quelle que soit la façon dont je configurais l'attente, ce qui conduisait à un blocage.

La solution a donc consisté à créer ma propre version du code de la bibliothèque externe. ExternalLibraryStringAsync afin qu'il ait les propriétés de continuation souhaitées.


mauvaise réponse à des fins historiques

Après beaucoup de douleur et d'angoisse, j'ai trouvé la solution. enterré dans cet article de blog (Ctrl-f pour 'deadlock'). Il s'agit d'utiliser task.ContinueWith au lieu de la simple task.Result .

Exemple de blocage antérieur :

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

Évitez l'impasse comme ceci :

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

2voto

Ramin Points 16

Réponse rapide : modifier cette ligne

ResultClass slowTotal = asyncTask.Result;

a

ResultClass slowTotal = await asyncTask;

pourquoi ? vous ne devriez pas utiliser .result pour obtenir le résultat de tâches dans la plupart des applications, sauf dans les applications console ; si vous le faites, votre programme se bloquera lorsqu'il y parviendra

vous pouvez également essayer le code ci-dessous si vous voulez utiliser .Result

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X