web-dev-qa-db-fra.com

Pourquoi cette action asynchrone se bloque-t-elle?

J'ai une application .NET 4.5 à plusieurs niveaux appelant une méthode à l'aide des nouveaux mots-clés async et await de C # 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 (essentiellement un wrapper pour les objets DBConnection et DBCommand sous-jacents):

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 ceci 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 d'interface utilisateur (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, c'est qu'il est bloqué pour toujours sur cette dernière ligne. Il en va de même si j'appelle asyncTask.Wait(). Si j'exécute directement la méthode SQL lente, cela prend environ 4 secondes.

Le comportement auquel je m'attends est que, lorsqu'il arrive à asyncTask.Result, S'il n'est pas terminé, il doit attendre jusqu'à ce qu'il le soit et, une fois que c'est le cas, il devrait renvoyer le résultat.

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

Une idée de ce que je fais mal?

Avez-vous des suggestions sur les domaines dans lesquels je dois enquêter pour résoudre ce problème?

Est-ce que cela pourrait être une impasse quelque part, et si oui, existe-t-il un moyen direct de le trouver?

95
Keith

Oui, c'est une impasse, d'accord. Et une erreur commune avec le TPL, alors ne vous sentez pas mal.

Lorsque vous écrivez await foo, Le moteur d'exécution planifie par défaut la poursuite de la fonction sur le même SynchronizationContext que celui sur lequel la méthode a démarré. En anglais, supposons que vous appeliez votre ExecuteAsync à partir du fil de l'interface utilisateur. Votre requête s'exécute sur le thread threadpool (parce que vous avez appelé Task.Run), Mais vous attendez alors le résultat. Cela signifie que le moteur d'exécution planifie votre ligne "return result;" Pour qu'elle soit réexécutée sur le thread d'interface utilisateur, plutôt que de la planifier dans le pool de threads.

Alors, comment se fait cette impasse? Imaginez que vous ayez juste ce code:

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

La première ligne lance donc le travail asynchrone. La deuxième ligne alors bloque le fil de l'interface utilisateur. Ainsi, lorsque le moteur d'exécution souhaite exécuter la ligne "return result" sur le thread d'interface utilisateur, il ne peut le faire que jusqu'à ce que le Result soit terminé. Mais bien sûr, le résultat ne peut être donné tant que le retour n’est pas effectué. Impasse.

Ceci illustre une règle clé d'utilisation de la TPL: lorsque vous utilisez .Result Sur un thread d'interface utilisateur (ou dans un autre contexte de synchronisation sophistiqué), vous devez faire attention à ce que rien ne dépend de cette tâche n'est planifié dans l'interface utilisateur. fil. Ou alors le mal est arrivé.

Donc que fais-tu? L'option n ° 1 est l'utilisation attend partout, mais comme vous l'avez dit, ce n'est déjà pas une option. La deuxième option qui est disponible pour vous est simplement de cesser d'utiliser attendre. Vous pouvez réécrire vos deux fonctions pour:

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 maintenant aucune attente, donc rien n'est implicitement planifié sur le thread d'interface utilisateur. Pour les méthodes simples comme celles-ci qui ont un seul retour, il est inutile de faire un motif "var result = await...; return result"; supprimez simplement le modificateur asynchrone et transmettez directement l’objet tâche. C'est moins de frais généraux, si rien d'autre.

L'option n ° 3 consiste à spécifier que vous ne souhaitez pas que votre attente soit planifiée dans le thread d'interface utilisateur, mais simplement dans le thread d'interface utilisateur. Vous faites cela avec la méthode 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);
}

Si vous attendez une tâche, vous devrez normalement programmer le thread d'interface utilisateur si vous y êtes; attendre le résultat de ContinueAwait ignorera le contexte dans lequel vous vous trouvez et planifie toujours en fonction du pool de threads. L'inconvénient de ceci est que vous devez saupoudrer ceci partout dans toutes les fonctions dont dépend votre résultat. Les résultats manqués .ConfigureAwait Pourraient être la cause d'un autre blocage.

139
Jason Malinowski

C’est le scénario classique de blocage -async mixte, comme je le décris sur mon blog . Jason l'a bien décrit: par défaut, un "contexte" est enregistré à chaque await et est utilisé pour continuer la méthode async. Ce "contexte" est le SynchronizationContext actuel, à moins que ce ne soit null, auquel cas il s’agit du TaskScheduler actuel. Lorsque la méthode async tente de continuer, elle entre d'abord dans le "contexte" capturé (dans ce cas, un ASP.NET SynchronizationContext). ASP.NET SynchronizationContext n'autorise qu'un seul thread à la fois dans le contexte. Un thread existe déjà dans le contexte - le thread est bloqué sur Task.Result.

Il existe deux directives pour éviter cette impasse:

  1. Utilisez async complètement. Vous dites que vous "ne pouvez pas" faire cela, mais je ne suis pas sûr de savoir pourquoi. ASP.NET MVC sur .NET 4.5 peut certainement prendre en charge async actions, et ce n'est pas un changement difficile à faire.
  2. Utilisez ConfigureAwait(continueOnCapturedContext: false) autant que possible. Cela annule le comportement par défaut de reprise sur le contexte capturé.
34
Stephen Cleary

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

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?

11
Danilow