7

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.
}
  • Asynchronous programming is an orthogonal concept to threading -- the two are often used together, but asynchrony itself is more about orchestrating code in a way which responds to events (after having been initiated by the same code). There's no requirement for those events to occur on a different thread, and indeed it works rather well in single-threaded code. Async/await is a mechanism to orchestrate 'event driven' behaviour (for example, receipt of an HTTP response or completion of a DB query). The common alternative would be to orchestrate that code using callbacks instead. – Ben Cottrell Jun 13 at 6:53
  • @BenCottrell indeed, I'm looking at this from a Java perspective where asynchronous is often synonymous with multi-threaded, but they're of course not the same thing. Threading is one way of achieving asynchronicity. I'm not very familiar with the inner workings of C#, and the discrepancy between (what I see as) "normal" Java vs. C# seems a bit odd to me. Javascript callbacks on the other hand make sense since the environment is single threaded. Hence my question about whether C# uses a more "clever" threading model (or async model rather). – Kayaman Jun 13 at 7:17
  • @Kayaman It's not really that one language has a more clever way of handling it than the other. It's mostly syntax differences. From the point of view of assembly (at least concerning java and C#), what is happening is very similar, I assure you. – Neil Jun 13 at 7:23
  • The C# example is of bad code, because it is synchronous disguised as async – Caleth Jun 13 at 8:42
  • @Caleth yet it's given as an example of perfectly acceptable code (and if I understood Sebastian correctly, the thread doesn't "go to waste"). The Java code is suboptimal, because at the least you could just submit the tasks to an executor and continue on after the loop. Although that might not give you much advantage, if you need to let's say verify that all the mails were sent without problems. – Kayaman Jun 13 at 8:55
10

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.

  • Ah thank you! I now remember that I have read about coroutines in C#, but as you said Java has no equivalent (you'd be responsible for finding work for the current thread, which isn't always possible), so I didn't get my head around it right away thinking from my perspective. Thank you for your clear explanation. – Kayaman Jun 13 at 8:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.