web-dev-qa-db-fra.com

La construction C # async / Task est-elle équivalente à Java Executor / Future?

Je suis depuis longtemps Java, mais avec si peu de trafic sur SE, je ne limite pas mon affichage à des balises simples. J'ai remarqué que les questions C # avec async/attendent viennent beaucoup, et pour autant que je l'ai lu, c'est le mécanisme de programmation asynchrone standard (mais quelque peu récent) en C #, ce qui le rendrait équivalent à Executor, Future ou peut-être plus précisément de Java CompatibleFuture constructions (ou même wait()/notify() pour un code très ancien).

Maintenant, async/wait semble être un outil pratique, rapide à écrire et facile à comprendre, mais les questions me donnent l'impression que les gens essaient de tout faire async, et que ce n'est même pas une mauvaise chose.

En Java, le code qui tenterait de décharger le travail sur différents threads aussi souvent que possible semblerait très étrange, et les avantages réels en termes de performances proviennent d'endroits spécifiques bien pensés où le travail est divisé en différents threads.

L'écart est-il dû à un modèle de filetage différent? Parce que C # est souvent du code de bureau avec une plate-forme bien connue sous-jacente, alors que Java est principalement côté serveur ces jours-ci? Ou ai-je juste obtenu une image biaisée de sa pertinence?

De https://markheath.net/post/async-antipatterns nous avons:

foreach(var c in customers)
{
    await SendEmailAsync(c);
}

comme exemple de code acceptable, mais cela semble suggérer "rendre tout asynchrone, vous pouvez toujours utiliser await pour synchroniser". Est-ce parce que le await indique déjà qu'il n'y a en fait aucune raison de changer de thread, alors qu'en Java

for(Customer c : customer) {
    Future<Result> res = sendEmailAsync(c);
    res.get();
}

effectuerait en fait le travail dans un thread différent, mais get() bloquerait, donc ce serait un travail séquentiel, mais en utilisant 2 threads?

Bien sûr, avec Java, l'idiome standard serait de laisser l'appelant décider si quelque chose doit être asynchrone, plutôt que de demander à l'implémentation de le décider à l'avance.

for(Customer c : customer) {
    CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> sendEmailSync(c));
    // cf can be further used for chaining, exception handling, etc.
}
7
Kayaman

Task de C # est quelque part à mi-chemin entre Future et CompletableFuture de Java. La propriété Result équivaut à appeler get(), ContinueWith() fait ce que fait le tableau massif de fonctions de continuation sur CompletableFuture (ajoutez des Task.WhenAny Et Task.WhenAll Là-dedans). Mais complete(T) n'a pas d'équivalent (utilisez un TaskCompletionSource), pas plus que cancel() (passez des CancellationToken explicites).

Executor de Java est principalement caché en C #. Il existe un pool de threads global équivalent à un ForkJoinPool qui est automatiquement utilisé par Task.Run. Il y a TaskScheduler si vous voulez vraiment contrôler l'exécution, mais vous l'utilisez très rarement.

async/await, cependant, n'a pas d'équivalent en Java. Le point clé ici est que await someTask Est pas identique à someFuture.get(). Ce dernier bloque le thread d'exécution jusqu'à ce que l'avenir soit complet.

Le premier fait quelque chose de complètement différent. Conceptuellement, il implémente quelque chose comme des coroutines. Si la tâche n'est pas terminée, elle suspend l'exécution de la fonction actuelle, mais libère le thread pour continuer à travailler sur d'autres choses. Ce n'est que lorsque la tâche est terminée que l'exécution continue à partir du point de await.

En termes pratiques, c'est une transformation de compilateur. Le compilateur divise une fonction async en morceaux. Lorsque vous appelez la fonction, la première pièce s'exécute jusqu'à ce qu'elle atteigne un await. Si la tâche est terminée, elle exécute simplement la pièce suivante. Sinon, il utilise ContinueWith pour planifier l'exécution de la prochaine pièce une fois la tâche terminée.

Il s'agit d'une distinction importante, car cela signifie que si la chose que vous attendez est bloquée sur l'accès au réseau, vous ne mangez pas un thread du pool; à la place, le thread peut travailler sur d'autres tâches.

(La description ci-dessus est simplifiée. Le compilateur ne divise pas réellement la fonction, il la transforme en une machine d'état. Et il n'utilise pas ContinueWith, mais GetAwaiter().UnsafeOnCompleted.)

Cela signifie que vous obtenez l'efficacité des continuations, mais avec le modèle de programmation pratique des fonctions normales.

11
Sebastian Redl