web-dev-qa-db-fra.com

Pourquoi dois-je utiliser ConfigureAwait (false) dans toute la fermeture transitive?

J'apprends asynchrone/attendre et après avoir lu cet article Ne pas bloquer sur le code asynchrone

et ceci Async/wait convient-il aux méthodes qui sont à la fois IO et lié au CP

Je remarque un conseil de l'article de @Stephen Cleary.

L'utilisation de ConfigureAwait (false) pour éviter les blocages est une pratique dangereuse. Vous devez utiliser ConfigureAwait (false) pour chaque attente dans la fermeture transitive de toutes les méthodes appelées par le code de blocage, y compris tous les codes tiers et tiers. L'utilisation de ConfigureAwait (false) pour éviter un blocage n'est au mieux qu'un hack).

Il est apparu à nouveau dans le code de la poste comme je l'ai joint ci-dessus.

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync()
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
        return LoadHtmlDocument(contentStream); //CPU-bound
}

Comme je le sais lorsque nous utilisons ConfigureAwait (false), le reste de la méthode asynchrone sera exécuté dans le pool de threads. Pourquoi devons-nous l'ajouter à chaque attente de fermeture transitive? Je pense moi-même que c'est la bonne version comme ce que je savais.

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)
        .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
        return LoadHtmlDocument(contentStream); //CPU-bound
}

Cela signifie que la deuxième utilisation de ConfigureAwait (false) dans l'utilisation de block est inutile. Veuillez me dire la bonne façon. Merci d'avance.

24
vietvoquoc

Comme ma connaissance lorsque nous utilisons ConfigureAwait(false) le reste de la méthode asynchrone sera exécuté dans le pool de threads.

Fermer, mais il y a une mise en garde importante qui vous manque. Lorsque vous reprenez après avoir attendu une tâche avec ConfigureAwait(false), vous reprendrez sur un thread arbitraire. Prenez note des mots "lorsque vous reprendrez."

Laisse moi te montrer quelque chose:

public async Task<string> GetValueAsync()
{
    return "Cached Value";
}

public async Task Example1()
{
    await this.GetValueAsync().ConfigureAwait(false);
}

Considérez le await dans Example1. Bien que vous attendiez une méthode async, cette méthode n'effectue en fait aucun travail asynchrone. Si une méthode async ne fait rien await, elle s'exécute de manière synchrone et l'attente ne reprend jamais car elle ne suspendu en premier lieu. Comme le montre cet exemple, les appels à ConfigureAwait(false) peuvent être superflus: ils peuvent n'avoir aucun effet. Dans cet exemple, quel que soit le contexte dans lequel vous vous trouviez lorsque vous saisissez Example1 Est le contexte dans lequel vous vous trouverez après le await.

Pas tout à fait ce que vous attendiez, non? Et pourtant, ce n'est pas tout à fait inhabituel. De nombreuses méthodes async peuvent contenir des chemins rapides qui ne nécessitent pas la suspension de l'appelant. La disponibilité d'une ressource mise en cache est un bon exemple (merci, @ jakub-dąbek!), Mais il existe de nombreuses autres raisons pour lesquelles une méthode async peut être mise en sécurité plus tôt. Nous vérifions souvent diverses conditions au début d'une méthode pour voir si nous pouvons éviter de faire un travail inutile, et les méthodes async ne sont pas différentes.

Regardons un autre exemple, cette fois à partir d'une application WPF:

async Task DoSomethingBenignAsync()
{
    await Task.Yield();
}

Task DoSomethingUnexpectedAsync()
{
    var tcs = new TaskCompletionSource<string>();
    Dispatcher.BeginInvoke(Action(() => tcs.SetResult("Done!")));
    return tcs.Task;
}

async Task Example2()
{
    await DoSomethingBenignAsync().ConfigureAwait(false);
    await DoSomethingUnexpectedAsync();
}

Jetez un œil à Example2. La première méthode que nous await exécute toujours de manière asynchrone. Au moment où nous atteignons le deuxième await, nous savons que nous fonctionnons sur un thread de pool de threads, donc il n'y a pas besoin de ConfigureAwait(false) sur le deuxième appel, non? Mauvais. Malgré le fait d'avoir Async dans le nom et de renvoyer un Task, notre deuxième méthode n'a pas été écrite en utilisant async et await. Au lieu de cela, il effectue sa propre planification et utilise un TaskCompletionSource pour communiquer le résultat. Lorsque vous reprenez votre await, vous pourriez[1] finissent par s'exécuter sur le thread qui a fourni le résultat, qui dans ce cas est le thread de répartiteur de WPF. Oups.

Le point clé à retenir ici est que, souvent, vous ne savez pas exactement ce que fait une méthode "attendue". Avec ou sans CongifureAwait, vous pourriez vous retrouver dans un endroit inattendu. Cela peut se produire à n'importe quel niveau d'une pile d'appels async, donc le moyen le plus sûr pour éviter de s'approprier par inadvertance un contexte à thread unique est d'utiliser ConfigureAwait(false) avec tous les await, c'est-à-dire tout au long de la fermeture transitive.

Bien sûr, il peut y avoir des moments où vous voulez reprendre sur votre contexte actuel, et c'est très bien. C'est apparemment pourquoi c'est le comportement par défaut. Mais si vous n'en avez pas vraiment besoin, alors je recommande d'utiliser ConfigureAwait(false) par défaut. Cela est particulièrement vrai pour le code de bibliothèque. Le code de bibliothèque peut être appelé de n'importe où, il est donc préférable d'adhérer au principe de la moindre surprise. Cela signifie ne pas verrouiller les autres threads hors du contexte de votre appelant lorsque vous n'en avez pas besoin. Même si vous utilisez ConfigureAwait(false) partout dans votre code de bibliothèque, votre appelant aura toujours la possibilité de reprendre sur son contexte d'origine si c'est ce qu'ils veulent.

[1] Ce comportement peut varier selon le framework et la version du compilateur.

27
Mike Strobel