3 ms·
It is never semantically important (in a context that is multithreaded anyway) because you don't have more control over scheduling with async/await than you do
by pron 6y ago
It is never semantically important (in a context that is multithreaded anyway) because you don't have more control over scheduling with async/await than you do with Loom's virtual threads. In either case, the code doesn't know if scheduling takes place on a single kernel thread or multiple ones. Nor does it know about the existence of other threads running concurrently. There could be some technical differences, like various GPU drivers allowing only specific kernel threads to use it, but Loom answers that with pluggable schedulers.
The difference in the native call case isn't semantic; it's, at worst, a difference in performance [1], and as far as I know, no one is making that distinction today, so there's nothing to erase. Moreover, as Java doesn't have async/await, there isn't even an artificial distinction to erase.
I don't know all the reasons Java abandoned green threads. One of them was that the classic VM that had green threads was simply replaced with HotSpot; it wasn't an evolution of a single VM. But more practically, and putting aside the fact that it was M:1, at the time there Java code relied on native FFI a great deal, whereas today it is very rare in general.
[1]: This is a little inaccurate. There could be a difference in liveness properties (deadlock) in that case, but neither async/await nor kernel threads, for that matter, make intrinsic liveness guarantees. You have to trust your scheduler in all cases.
- lmm 6y ago> you don't have more control over scheduling with async/await than you do with Loom's virtual threads. In either case, the code doesn't know if scheduling takes place on a single kernel thread or multiple ones. You do have that control: the value of having explicitly async functions is that it becomes possible to have non-async functions (just as the value of having explicitly nullable values is that it becomes possible to have non-nullable values, and the value of having explicitly-exception-throwing functions is that it becomes possible to have non-exception-throwing functions). A non-async function is nothing more or less than a function that is guaranteed to execute on a single native thread, whereas an async function might be executed over multiple native threads; in particular after `val x = f()` execution will continue on the same native thread as before, whereas after `val x = await f()` execution might continue on a different native thread. I hope it works out, but there just seems to be so much opportunity for unforeseen edge cases; something very fundamental and global that every (post-1.2) Java programmer is used to knowing will no longer be true.
- pron 6y agoEverything on a thread always executes on the same thread, whether or not it's virtual; that's an invariant of threads. You cannot observe (unless it's a bug on our part or you're doing something complicated and deliberate) that, when running on a virtual thread, you're actually running on multiple native threads any more than you can observe moving between processors; if you could, that would indeed be a problem. The JDK hides the native thread the same way the native thread hides the processor. That async/await allows this implementation detail to leak is not a feature, it's a bug. It forces a syntactic distinction without a semantic difference (well, it introduces a semantic difference as a bad side effect of a technical decision -- the semantic difference is that you can observe the current thread changing underneath you, which, in turn, makes the syntactic difference beneficial; in other words, it's a syntactic "solution" to a self-imposed, avoidable problem).
- lmm 6y ago> That async/await allows this implementation detail to leak is not a feature, it's a bug. It forces a syntactic distinction without a semantic difference (well, it introduces a semantic difference as a bad side effect of a technical decision -- the semantic difference is that you can observe the current thread changing underneath you, which, in turn, makes the syntactic difference beneficial; in other words, it's a syntactic "solution" to a self-imposed, avoidable problem). You're putting the cart before the horse. On the assumption that the programmer needs to be able to control native threading behaviour in at least some cases (if only a small minority), surfacing the details of the points at which evaluation may cross native thread boundaries is useful and important, and async/await is the minimally-intrusive way to achieve that. If it's really possible to abstract over the native threading completely seamlessly such that the programmer never needs to look behind the curtain, then async/await is pointless syntactic complexity - just as if it were possible to write error-free code then exceptions would be pointless syntactic complexity. But if there's still a need to control native threading behaviour then this is going to end up being done by some kind of magic scheduler hints that aren't directly visible in the code and will be easy to accidentally disrupt by refactoring, and in that case I'd rather have async/await (as long as I've got a way to be polymorphic over it).