6 ms·
The section "What about async/await?", which compares these virtual threads to async/await is very weak. After reading this article, I came away with the impres
by mgraczyk 4y ago
The section "What about async/await?", which compares these virtual threads to async/await is very weak. After reading this article, I came away with the impression that this is a dramatically worse way to solve this problem than async/await. The only benefit I see is that this will be simpler to use for the (increasingly rare) programmers who are not used to async programming.
The first objection in the article is that with async/await you to may forget to use an async operation and could instead use a synchronous operation. This is not a real problem. Languages like JavaScript do not have any synchronous operations so you can't use them by mistake. Languages like python and C# solve this with simple lint rules that tell you if you make this mistake.
The second objection is that you have to reimplement all library functions to support await. This is a bad objection because you also have to do this for virtual threads. Based on how long it took to add virtual threada to Java vs adding async/await to other languages, it seems like virtual threads were much more complicated to implement.
The programming model here sounds analogous to using gevent with python vs python async/await. My opinion is that the gevent approach will die out completely as async/await becomes better supported and programmers become more familiar.
EDIT: Looking more at the "Related Work" section at the bottom. I think I understand the problem here. The "Structured Concurrency" examples are unergonomical versions of async/await. I'm not sure what I'm missing but this seems like a strictly worse way to write structured concurrent code.
Java example:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> findUser());
Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // Join both forks
scope.throwIfFailed(); // ... and propagate errors
// Here, both forks have succeeded, so compose their results
return new Response(user.resultNow(), order.resultNow());
}
}
Python equivalent
async def handle() -> Response:
# scope is implicit, throwing on failure is implicit.
user, order = await asyncio.gather(findUser(), findOrder())
return Response(user, order)
You could probably implement a similar abstraction in Java, but you would need to pass around and manage the the scope object, which seems cumbersome.
- wtetzner 4y agoI can see you having objections to their arguments against async/await, but what makes you say async/await is somehow the better solution?
- mgraczyk 4y agoThere are a few reasons. async/await allows you to do multiple things in parallel. I don't see how you can do that in the virtual threading model, although I haven't used it and only read this article. You would have to spin up threads and wait for them to finish, which IMO is much more complicated and hard to read. javascript async function doTwoThings() { await Promise.all([ doThingOne(), doThingTwo(), ]); } python async def do_two_things() { await asyncio.gather( do_thing_one(), do_thing_two(), ); } Another issue is building abstractions on top of this. For example how do you implement "debounce" using virtual threads? You end up unnaturally reimplementing async/await anyway. Finally it's generally much easier implement new libraries with a promise/future based async/await system than with a system based on threads, but I'm not familiar enough with Java to know whether this is actually a good objection. It's possible they make it really easy.
- Matthias247 4y agoNote that does do not natively reflect structured concurrency - and a Promise.any version does even less so. The code above would „return“ once the first Promise runs to completion. The computation that is described by the second Promise would however continue to run - which could cause unexpected side effects. The goal of structured concurrency is to totally avoid those by making sure the lifetime of child tasks is always contained within the lifetime of their parent tasks. Bare async/await only marginally guarantees that, and the amount depends on the language. Eg the JS code above will never guarantee it, since each async function is really a separate entity on the eventloop. Rust async/await can do it if you all use are low level future combinators (like futures::join! or select!. But if the code spawns any child tasks then all bets are off too. The Java example with scoped executor will have proper guarantees. Waiting on the executor means waiting for all tasks to complete. And as soon as the first task errors other tasks are „asked“ to stop early via an InterruptedException.