Java – Is the C# async/Task construct equivalent to Java’s Executor/Future

asyncasynchronous-programmingcjava

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's Future and CompletableFuture. The Result property is equivalent to calling get(), ContinueWith() does the things the massive array of continuation functions on CompletableFuture does (add some Task.WhenAny and Task.WhenAll in there). But complete(T) has no equivalent (use a TaskCompletionSource), nor does cancel() (pass explicit CancellationTokens).

Java's Executor is mostly hidden in C#. There's a global thread pool equivalent to a ForkJoinPool that is automatically used by Task.Run. There's TaskScheduler 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 that await someTask is not the same as someFuture.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 an await. If the task is done, it simply executes the next piece. Otherwise, it uses ContinueWith 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, but GetAwaiter().UnsafeOnCompleted.)

This means you get the efficiency of continuations, but with the convenient programming model of normal functions.