I'm a long time Java developer, but with so little traffic on SE, I don't limit my viewing to any single tags. I've noticed that C# questions with async/await come up a lot, and as far as I've read it's the standard (but somewhat recent) asynchronous programming mechanism in C#, which would make it equivalent to Java's Executor
, Future
or perhaps more accurately CompatibleFuture constructs (or even wait()/notify()
for very old code).
Now async / await seems like a handy tool, quick to write and easy to understand, but the questions give me the feeling that people are trying to make everything async, and that this isn't even a bad thing.
In Java, code that would attempt to offload work to different threads as often as possible would seem very odd, and real performance advantages come from specific well thought out places where work is divided to different threads.
Is the discrepancy because of a different threading model? Because C# is often desktop code with a well known platform under it, whereas Java is mainly server side these days? Or have I just gotten a skewed image of its relevancy?
From https://markheath.net/post/async-antipatterns we have:
foreach(var c in customers)
{
await SendEmailAsync(c);
}
as an example of acceptable code, but this seems to suggest "make everything asynchronous, you can always use await
to synchronize". Is it because the await
already indicates that there's actually no reason to switch threads, whereas in Java
for(Customer c : customer) {
Future<Result> res = sendEmailAsync(c);
res.get();
}
would actually perform the work in a different thread, but get()
would block, so it would be sequential work, but using 2 threads?
Of course with Java, the standard idiom would be to let the caller decide whether something needs to be async, rather than having the implementation decide it in advance E.g.
for(Customer c : customer) {
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> sendEmailSync(c));
// cf can be further used for chaining, exception handling, etc.
}
Best Answer
C#'s
Task
is somewhere halfway between Java'sFuture
andCompletableFuture
. TheResult
property is equivalent to callingget()
,ContinueWith()
does the things the massive array of continuation functions onCompletableFuture
does (add someTask.WhenAny
andTask.WhenAll
in there). Butcomplete(T)
has no equivalent (use aTaskCompletionSource
), nor doescancel()
(pass explicitCancellationToken
s).Java's
Executor
is mostly hidden in C#. There's a global thread pool equivalent to aForkJoinPool
that is automatically used byTask.Run
. There'sTaskScheduler
if you really want to control execution, but you very rarely use it.async
/await
, though, has no equivalent in Java. The key point here is thatawait someTask
is not the same assomeFuture.get()
. The latter blocks the executing thread until the future is complete.The former does something completely different. Conceptually, it implements something like coroutines. If the task is not complete, it suspends execution of the current function, but frees the thread up to continue working on other things. Only once the task is complete does execution continue from the point of the
await
.In practical terms, it's a compiler transformation. The compiler divides an
async
function into pieces. When you call the function, the first piece executes, until it reaches anawait
. If the task is done, it simply executes the next piece. Otherwise, it usesContinueWith
to schedule the next piece to run once the task completes.This is an important distinction, because it means that if the thing you're awaiting is blocked on network access, you're not eating up a thread of the pool; instead the thread can work on other tasks.
(The above description is simplified. The compiler doesn't actually split the function, it turns it into a state machine. And it doesn't use
ContinueWith
, butGetAwaiter().UnsafeOnCompleted
.)This means you get the efficiency of continuations, but with the convenient programming model of normal functions.