News for hackers. Stuff that matters.
Re:State of Loom by ceronman on Saturday May 16, 2020 @07:43AM (#23202629)
This is my favorite JVM project and I think it's going to be huge!

This model of concurrency is so much better than the async/await model used by many other languages. No more colored functions [1], or worse Completable<Future>, promises et al. Nice stacktraces, debuggers that work plus no need for thread pools anymore. I can't wait for this to be ready for production.

Only drawback seems to be when calling native code. I guess it's the same problem that Golang has. Good thing is that the Java ecosystem is not that dependent on native stuff, so I think it's a fair tradeoff to make.

[1] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...

Re:State of Loom by mrkeen on Saturday May 16, 2020 @08:22AM (#23202812)
> No more colored functions [1]

That's a property of the function, whether or not you want the type system to help you with it. I can call a function myRedFunction, but if it calls a blue function, it's blue.

> or worse Completable<Future> What is this? Future<Result> f = e.submit(() -> { ... return result; }); // spawns a new virtual thread

> This model of concurrency is so much better than the async/await mode

  ThreadFactory tf = Thread.builder().virtual().factory();
  ExecutorService e = Executors.newUnboundedExecutor(tf);
  Future<Result> f = e.submit(() -> { ... return result; }); // spawns a new virtual thread
  ...
  Result y = f.get(); // joins the virtual thread
I don't get it. This just looks like async/await (submit/get) with extra steps. Why not just write

  Future<Result> f = async ( ... result; )
  Result y = await (f);
Re:State of Loom by pron on Saturday May 16, 2020 @08:26AM (#23202844)
> That's a property of the function, whether or not you want the type system to help you with it.

The problem is that in, say, C# or Kotlin, subroutines with the same semantics come in two different colours. The type system "helps" you distinguish between things that aren't meaningfully distinguishable.

> I don't get it. This just looks like async/await (submit/get) with extra steps.

You only need to submit if you want to do stuff in parallel and then join (also, there are no extra steps). The analogue to:

    var a = await doA();
    var b = await doB();
is just:

    var a = doA();
    var b = doB();
Re:State of Loom by lmm on Saturday May 16, 2020 @09:38AM (#23203269)
> The problem is that in, say, C# or Kotlin, subroutines with the same semantics come in two different colours. The type system "helps" you distinguish between things that aren't meaningfully distinguishable.

The distinction is important though, because it affects the observable behaviour of other effects present in these functions. https://glyph.twistedmatrix.com/2014/02/unyielding.html

The only cases where you want the same function to come in two colour variants is when it's a higher-order function, and in that case what you really want is not to ignore colour but for the function to be polymorphic over colour. (i.e. higher-kinded types).

Re:State of Loom by pron on Saturday May 16, 2020 @09:50AM (#23203361)
> The distinction is important though, because it affects the observable behaviour of other effects present in these functions.

Only if the language is single-threaded to begin with, like JavaScript.[1] There is no real difference in C# and Kotlin between, say, sleep and delay. It's a syntactic distinction between semantically equivalent terms (for all practical purposes).

[1]: Even then there are better ways to reason about effects.

Re:State of Loom by lmm on Saturday May 16, 2020 @10:18AM (#23203555)
Hmm. I find it hard to believe that whether a given block of code executes on a single OS-level thread or yields and is rescheduled across multiple OS-level threads is never semantically important (to resort to an extreme example, one kind of function can be used in a callback that's invoked by native code and the other can't) - particularly given how many languages (including Java!) started with a green threading model only to later abandon it. And so I'm very dubious about erasing that distinction in low level functions, even if it's not relevant in the vast majority of cases, because once you erase it it's impossible to ever recover it at higher level.
Re:State of Loom by pron on Saturday May 16, 2020 @10:28AM (#23203629)
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.

Re:State of Loom by lmm on Saturday May 16, 2020 @10:50AM (#23203827)
> 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.

Re:State of Loom by pron on Saturday May 16, 2020 @10:56AM (#23203872)
Everything 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).

Re:State of Loom by lmm on Saturday May 16, 2020 @09:54PM (#23208805)
> 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).

Re:State of Loom by pron on Sunday May 17, 2020 @02:55AM (#23210152)
I reject both assumption and conclusion. To the limited extent such control is needed, virtual threads with pluggable schedulers are superior in every way, and certainly less intrusive. There aren't any "magic scheduler hints", either. You can assign a scheduler to each virtual thread, and that scheduler makes the relevant decisions. That's the same separation of concerns that native threads have, too.

The only place where this could be important is when dealing with Java code that cares about the identity of the native thread by querying it directly (because native code will see the native thread, but it's hidden from Java code). But, 1. such code is very rare, 2. almost all of it is in the JDK, which we control, and 3. even if some library somewhere does it, then that particular library will need to change to support virtual threads well, just as we require for most new big Java features.

Having said that, I don't claim async/await isn't a good solution for some platforms, and in some cases there's little choice. For example, Kotlin has no control of the JDK, which is necessary for lightweight threads, and languages with pointers into the stack as well as no GC and lots of FFI might find that a user-mode thread implementation is too costly. But I think that in cases where usermode threads can be implemented efficiently and in a way thar interacts well with the vast majority of code, they are clearly the preferable choice.

Re:State of Loom by lmm on Sunday May 17, 2020 @05:01AM (#23210693)
> There aren't any "magic scheduler hints", either. You can assign a scheduler to each virtual thread, and that scheduler makes the relevant decisions. That's the same separation of concerns that native threads have, too.

I'm thinking of when you need to "pin" to a particular native thread. In a native-threads + async/await language you have direct and visible control over where that happens. In a green-threads language it's going to involve magic.

> Having said that, I don't claim async/await isn't a good solution for some platforms, and in some cases there's little choice. For example, Kotlin has no control of the JDK, which is necessary for lightweight threads, and languages with pointers into the stack as well as no GC and lots of FFI might find that a user-mode thread implementation is too costly. But I think that in cases where usermode threads can be implemented efficiently and in a way thar interacts well with the vast majority of code, they are clearly the preferable choice.

A priori I agree. I expected green threads in Rust to work out too. I just can't get past having seen it fail to work out so many times. Maybe this time is different.

Re:State of Loom by pron on Sunday May 17, 2020 @05:12AM (#23210734)
> In a green-threads language it's going to involve magic.

No magic, just a custom scheduler for that particular thread. For example:

    Thread.builder().virtual(Executors.newFixedThreadPool(1)).factory();
would give you a factory for virtual threads that are all scheduled on top of one native thread (I didn't pick a specific one because I wanted to use something that's already in the JDK, but it's just as simple).

> I expected green threads in Rust to work out too. I just can't get past having seen it fail to work out so many times. Maybe this time is different.

Implementing usermode threads in Java and in Rust is very different as their constraints are very different. Here are some differences: Rust has pointers into the stack, Java doesn't; allocating memory (on stack growth) in Rust is costly and needs to be tracked, while in Java it's a pointer bump and tracked by the GC; Rust runs on top of LLVM, while Java controls the backend; Rust code relies on FFI far more than Java. So both the constraints and implementation challenges are too different between them to be comparable.

Not to mention that they have different design goals: Rust, like all low-level languages, has low abstraction (implementation details are expressed in signatures) because, like all low-level languages, it values control more than abstraction. Java is a high-level language with a high level of abstraction -- there is one method call abstraction, and the JIT chooses the implementation; there is one allocation abstraction, and the GC and JIT choose the implementation -- and it values abstraction over control. So whereas, regardless of constraints, Rust happily lives with two distinct thread-like constructs, in Java that would be a failure to meet our goals (assuming, of course, meeting them is possible).

There are many things that work well in high-level languages and not in low-level ones and vice versa, and that's fine because the languages are aimed at different problem domains and environments. Java should be compared to other high-level languages, like Erlang and Go, where userspace threads have been working very well, to the great satisfaction of users.

Having said that, I suggest you take a look at coroutines in Zig, a language that, I think, brings a truly fresh perspective to low-level programming, and might finally be what we low-level programmers have been waiting for so many years.

Re:State of Loom by lmm on Monday May 18, 2020 @10:15AM (#23222560)
> No magic, just a custom scheduler for that particular thread.

But there's a spooky-action-at-a-distance between that scheduler and the code that's running on the thread. Code that's meant to be pinned (and may behave incorrectly if not pinned) looks no different from any other code.

> Not to mention that they have different design goals: Rust, like all low-level languages, has low abstraction (implementation details are expressed in signatures) because, like all low-level languages, it values control more than abstraction. Java is a high-level language with a high level of abstraction -- there is one method call abstraction, and the JIT chooses the implementation; there is one allocation abstraction, and the GC and JIT choose the implementation -- and it values abstraction over control. So whereas, regardless of constraints, Rust happily lives with two distinct thread-like constructs, in Java that would be a failure to meet our goals (assuming, of course, meeting them is possible).

Java isn't positioned as a high-level, high-abstraction language and that's not, IME, the user community it has. It's a language that offers high performance at the cost of being verbose and cumbersome - witness the existence of primitive types, the special treatment of arrays, the memory-miserly default numeric types, the very existence of null. I've heard much more about people using Java for low-latency mechanical-sympathy style code than people using it for high-abstraction use cases like scripting or workbooks. It's always been advertised as a safer alternative to C++ - rather like Rust.

(I'm all for trying to expand Java to be useful in other cases, but that's not grounds to sacrifice what it currently does well. For all the criticism Java attracts, it is undeniably extremely successful in its current niche)

Re:State of Loom by pron on Monday May 18, 2020 @12:52PM (#23224921)
> Code that's meant to be pinned (and may behave incorrectly if not pinned) looks no different from any other code.

Same goes for async/await. The decision to keep you running on the same native thread is up to the scheduler.

> Java isn't positioned as a high-level, high-abstraction language and that's not, IME, the user community it has.

I beg to differ. It aims to be a good compromise between productivity, observability and performance. Every choice, from JIT to GC, is about improving performance for the common case while helping productivity, not improving performance by adding fine-grained control. There are a few cases where this is not possible. Primitives is one of them, and, indeed, 25 years later, we're "expanding" primitives rather than find some automatic way for optimal memory layout.

> It's always been advertised as a safer alternative to C++ - rather like Rust.

I think you're mistaken, and in any event, this is certainly not our position. Java is designed for a good blend of productivity, observability and performance, and unless the circumstances are unusual, it opts for improving common-case performance with high abstractions rather than worst-case performance with low abstractions like C++/Rust. Roughly speaking, the stance on performance is how do we get to 95% with the least amount of programmer effort.

Anyway, regardless of what I said above, the constraints on the design of usermode threads other than philosophy are also very different for Java than for C++/Rust for reasons I mentioned. Still, Zig does it more like Java (despite still using the words async and await, but they mean something more like Loom's submit and join than what they mean in C#): https://youtu.be/zeLToGnjIUM

Re:State of Loom by lmm on Tuesday May 19, 2020 @07:51AM (#23233806)
> Same goes for async/await. The decision to keep you running on the same native thread is up to the scheduler.

The scheduler decides what happens at each yield point, but code that doesn't yield is guaranteed to stay pinned to a single native thread. A non-async function is somewhat analogous to a critical section; the difference between async and not is a visible distinction between must-run-on-a-pinned-native-thread functions and may-be-shuffled-between-native-threads functions.

Re:State of Loom by pron on Tuesday May 19, 2020 @12:05PM (#23236398)
Can you give an example where this matters -- i.e. it's useful and allowed to move between native threads but not between well-known points -- given that the identity of the carrier thread cannot leak to the virtual thread?
Re:State of Loom by mrkeen on Sunday May 17, 2020 @05:11AM (#23210731)
> distinguish between things that aren't meaningfully distinguishable.

Are these semantically the same?

  Runnable action1 = () -> System.out.println("foo");
  Runnable action2 = () -> System.out.println("bar");
  action1.run();
  action2.run();

  var action3 = runAsync(() -> System.out.println("foo"));
  var action4 = runAsync(() -> System.out.println("bar"));
  action3.join();
  action4.join();
> You only need to submit if you want to do stuff in parallel and then join (also, there are no extra steps).

I'm not comparing parallel to sequential. My point was that we're already doing parallel programming:

  CompletableFuture<Result> fx = CompletableFuture.supplyAsync(() -> result);
  CompletableFuture<Result> fy = CompletableFuture.supplyAsync(() -> result);
  Result x = fx.join();
  Result y = fy.join();
Parent doesn't like the existing parallel model. New model looks like:

  // > plus no need for thread pools anymore.
  ThreadFactory tf = Thread.builder().virtual().factory();
  ExecutorService e = Executors.newUnboundedExecutor(tf);

  // > No more [...] Completable<Future>, promises et al.
  Future<Result> fx = e.submit(() -> { ... return result; });
  Future<Result> fy = e.submit(() -> { ... return result; });
  Result x = fx.get();
  Result y = fy.get();
If going from the first to the second is an improvement, I don't understand the excitement. Especially when other languages look like:

  fx <- async (return 3)
  fy <- async (return 3)
  x  <- wait fx
  y  <- wait fy
Re:State of Loom by pron on Sunday May 17, 2020 @05:17AM (#23210757)
> Are these semantically the same?

No, but that's not what I meant. These are semantically the same:

    action1sync();
    action2sync();

    await action1async();
    await action2async();
> If going from the first to the second is an improvement, I don't understand the excitement.

The excitement is about going from my second example to the first.

> Especially when other languages look like:

It doesn't matter what they look like in the code. They suffer from all the problems I mention in the article: exceptions lose context, debuggers and profilers lose their effectiveness, APIs are split into two disjoint worlds. Virtual threads gives you code that doesn't just look synchronous, but behaves like that in every observable way.

What the code looks like on the screen is a very small portion of the problem we're trying to solve.

Re:State of Loom by h-cobordism on Saturday May 16, 2020 @09:24AM (#23203185)
(1) That sounds like a recipe to unintentionally miss a ton of concurrency. Having to put an `await` there is a great indicator that you're forcing an order of execution.

(2) Can you give an example of an async and a non-async subroutine that have "the same semantics"?

Re:State of Loom by pron on Saturday May 16, 2020 @09:53AM (#23203373)
1. There is no need for await. Threads imply sequential execution.

2. sleep vs. delay in either C# or Kotlin.

Re:State of Loom by nlawalker on Saturday May 16, 2020 @10:50AM (#23203826)
> 1. There is no need for await. Threads imply sequential execution.

How do you execute doA() and doB() concurrently? In C# you can do something like:

   var t1 = doA();
   var t2 = doB();
   await Task.WhenAll(new[] {t1, t2});
   var a = t1.Result;
   var b = t2.Result;
EDIT: Ah, never mind, I just saw "You only need to submit if you want to do stuff in parallel and then join" above.
Re:State of Loom by jerf on Saturday May 16, 2020 @09:14PM (#23208582)
The reality is that you rarely want to doA and doB concurrently, so optimizing syntax for that case is not useful, whereas you want to be able to call functions without having to worry about their color all the time, where "all the time" here is typically >1 time per function.

Many of you are perhaps scratching your head and going "What? But of course I concurrently do multiple things all the time!" But this is one of those cases where you grossly overestimate the frequency of exceptions precisely because they are exceptions, and so they stick out in your mind [1]. If you go check your code, I guarantee that either A: you are working in a rare and very stereotypical case not common to most code or B: you have huge swathes of promise code that just chains a whole bunch of "then"s together, or you await virtually every promise immediately, or whatever the equivalent is in your particular environment. You most assuredly are not doing something fancy with promises more than one time per function on average.

This connects with academic work that has showed that in real code, there is typically much less "implicit parallelism" in our programs than people intuitively think. (Including me, even after reading such work.) Even if you write a system to automatically go into your code and systematically finds all the places you accidentally specified "doA" and "doB" as sequential when they could have been parallel, it turns out you don't actually get much.

[1]: I have found this is a common issue in a lot of programmer architecture astronaut work; optimizing not for the truly most common case, but for the case that sticks out most in your mind, which is often very much not the most common case at all, because the common case rapidly ceases to be memorable. I've done my fair share of pet projects like that.

Re:State of Loom by tjalfi on Sunday May 17, 2020 @01:21PM (#23213474)
ParaSail[0] is a parallel language that is being developed by Ada Core Technologies. It evaluates statements and expressions in parallel subject to data dependencies.

The paper ParaSail: A Pointer-Free Pervasively-Parallel Language for Irregular Computations[1] contains the following excerpt.

"This LLVM-targeted compiler back end was written by a summer intern who had not programmed in a parallel programing language before. Nevertheless, as can be seen from the table, executing this ParaSail program using multiple threads, while it did incur CPU scheduling overhead, more than made up for this overhead thanks to the parallelism “naturally” available in the program, producing a two times speed-up when going from single-threaded single core to hyper-threaded dual core."

One anecdote proves nothing but I'm cautiously optimistic that newer languages will make it much easier to write parallel programs.

[0] http://www.parasail-lang.org/

[1] https://programming-journal.org/2019/3/7/

Re:State of Loom by jerf on Sunday May 17, 2020 @07:54PM (#23216687)
I hope so too.

I want to emphasize that what was discovered by those papers is that if you take existing programs and squeeze all the parallelism from them you possibly can safely and automatically, it doesn't get you very much.

That doesn't mean that new languages and/or paradigms may not be able to get a lot more in the future.

But I do think just bodging promises on to the side of an existing language isn't it. In general that's just a slight tweak on what we already had and you don't get a lot out of it.

Re:State of Loom by toast0 on Saturday May 16, 2020 @09:38PM (#23208726)
I would imagine doA() should do@ and return the result, but startA() would start it.

If you just need to wait for both to finish, something like this should do it:

   var t1 = startA();
   var t2 = startB();
   var a = finishA(t1);
   var b = finishB(t2);
finishX would naturally block until X is done.
Re:State of Loom by Skinney on Saturday May 16, 2020 @08:35AM (#23202894)
Well, the first two lines are usually written somewhere at the top level. You won't actually write that anytime you want something to be async. The last two lines are what you usually write, so most of the time, the number of lines of code are equivalent.

Second, Java doesn't have async/await today. If Java was to introduce it, it wouldn't be compatible with the code written today. The big benefit of project loom is that code written today will get all the benefits of async/await without changing any code. In fact, because it's not important to pool threads, it actually gets easier.

The big problem with async/await in languages like C# or even coroutines in Kotlin is that it doesn't mix with the "old" api. The get the benefits you need to do a huge refactor of your code, and you need to make sure that any library you pull in is compatible.

Java's approach seems much better.

Re:State of Loom by jayd16 on Saturday May 16, 2020 @11:46AM (#23204357)
You don't. You can call synchronous functions from async functions just fine and vice versa. The syntax isn't identical but I don't understand this split the world meme.
Re:State of Loom by toast0 on Saturday May 16, 2020 @09:30PM (#23208682)
In my experience with C#, I ran into apis I wanted to call that were async. I couldn't call them, even as Result foo = await async_thing(), unless my function was marked as async. I don't remember having a problem calling sync functions from async functions though.
Re:State of Loom by uryga on Saturday May 16, 2020 @10:03PM (#23208843)
afaik sync can call async if the language allows you to make an executor/event-loop. e.g. with python's asyncio it'd be something like

  def sync_foo():
    task = async_bar()
    loop = asyncio.new_event_loop()
    return loop.run_until_complete(task)
after all, unless the loop is built into the language (like JS), you need bootstrap the async code somehow
Re:State of Loom by jayd16 on Saturday May 16, 2020 @11:12PM (#23209249)
You can just use:

    task.RunSynchronously()
mildly annoying but far from impossibly split.
Re:State of Loom by Skinney on Sunday May 17, 2020 @03:34AM (#23210353)
But now you’re blocking the thread, so if this function is being used by an async function higher up in the function chain, it’s no longer async (unless that is changed as well.

There’s a split, because every step of an async process has to be async/await compatible or you lose all benefit. In a small code base, this might not be a big problem. Where I work it will never realisticly be done.

With project loom we won’t have to do anything or learn anything new. Just upgrade the java runtime and everything just works better :)

Re:State of Loom by J-Kuhn on Sunday May 17, 2020 @04:07PM (#23214937)
And use a different executor.

But yeah, the magic is: All code is now implicitly async (except native calls) - as if all code always had async in it's signature. Just - there is no need to now add this to the signature.

Re:State of Loom by eggsnbacon1 on Monday May 18, 2020 @09:40AM (#23222141)
>I don't get it. This just looks like async/await (submit/get) with extra steps. Why not just write

because altering the underlying thread pool can convert all existing code to fibers without syntax changes. This is the crux of "colored functions", two different signatures for async/non-async.

The implications for improving the performance of existing code are enormous. Most Java API's give you at least indirect control of the thread pool used. Which means they can be converted to fibers without updating the library.

In Java, executors are basically thread pools. If you can update the executor to use fibers the rest of the code is using fibers now. Your second example only converts a single call site to fibers. A thread pool could be used in hundreds of places.

In practice, this is a huge difference. I can update my Streams to use a fiber pool, and all across my app the hundreds/thousands of streams are using fibers now. Same story with REST and DB access. There's usually a single thread pool for each. A few lines of code to update the pools and my REST and DB calls are all using fibers.

Re:State of Loom by albertzeyer on Saturday May 16, 2020 @09:45AM (#23203321)
Can you maybe elaborate why this model is better than async/await? I have to admit, I don't really know Loom, but from a first glance, it looks like this is just (green) threads, and async/await is anyway an orthogonal concept to this, or not? But maybe I am confusing things here.

Can you maybe recommend a good overview over all the current concurrent approaches and concepts, like async/await, and alternatives, and their advantages and problems? I would love to get a more recent overview on this.

I had the impression that most recent languages follow on the async/await concept. At least JS and Python. Maybe also Rust? Go? Erlang? And there are probably libraries for C++ to do the same, or maybe it is already integrated? I have to admit that I did not fully follow all this development.

Re:State of Loom by GolDDranks on Saturday May 16, 2020 @10:02AM (#23203438)
Async/await split up your functions at their "blocking points" to fragments that can be run fragment-by-fragment, in an interleaved way by an executor / event loop, thus enabling high level of concurrency within a single OS thread.

The problem is that your split-up async function ceases to be a normal function. It doesn't use the stack in a similar way than normal functions. Furthermore, the executor / event loop is required for running those async functions.

So you can't call async functions from normal functions, because they aren't. You need to create an event loop and then hand the task over for it to process it. Your world splits up into sync and async functions.

Go and Erlang are going with the similar "lightweight thread" approach as Java's project Loom. On the other hand, JS, Python and C# have gone with async. I'm not sure if these languages actually need async, going with something similar as project Loom would have also fit, and would have been simpler from the user perspective, perhaps? But hindsight is 20/20.

Rust has also gone async, and I'd argue it's the only one of the pack who really have async/await as a necessity - this is because of two design requirements that differ from Java, C#, Python and JS: 1) must have native-level performance when calling foreign code that expects a C-like stack. Something like Loom doesn't provide that. 2) must not have an implicit default runtime.

Re:State of Loom by yrio on Saturday May 16, 2020 @08:03PM (#23208242)
Python is popular in data science, AI & those field depends on a lot of native libraries: NumPy, SciPy, etc. Maybe that's why they chose the async await model
Re:State of Loom by GolDDranks on Sunday May 17, 2020 @06:14AM (#23210972)
Yeah, sounds plausible. A lot of Python stuff is actually wrapped C / C++ / Fortran code, so it makes sense to optimize for FFI.
Re:State of Loom by int_19h on Saturday May 16, 2020 @10:05PM (#23208852)
Those other languages do async because they're less insular, and want to interoperate with the world outside of their own runtime.
Re:State of Loom by sk5t on Saturday May 16, 2020 @09:32PM (#23208692)
> The problem is that your split-up async function ceases to be a normal function. It doesn't use the stack in a similar way

Have you taken the CLR (a.k.a. .NET / C#) async await for a spin? Microsoft's done a great job of squaring away those details so the stack/thread discontinuity doesn't much matter.

Re:State of Loom by pjmlp on Sunday May 17, 2020 @02:37AM (#23210063)
It kind of does, specially if you don't take care with ConfigureAwait().
Re:State of Loom by ceronman on Saturday May 16, 2020 @10:42AM (#23203759)
As others have pointed out, the big problem of async/await is that it splits the world in two kinds of functions that can't call each other.

For example, in Python you have this http client library called `requests`. It's very easy to use, but it is synchronous and you can't use it in when doing async/await. Instead you have to use a similar library called aiohttp. And it's like that for every single library or function doing any sort of IO, you need two versions of everything. Same thing happens in Node.js. You have `spawn` which is asynchronous and `spawnSync` which is synchronous. And so on.

And it doesn't stop with functions, this division goes across the language it self. For example in Python you have a `for` loop and an `async for` loop. Same in JS, you have `for` and `for await`. C# also has it's own `await forech` construct. It's like splitting the world for no reason.

The great advantage of Loom is that you don't need a parallel language, you can use the same http clients, database clients, filesystem libraries that you were using in a synchronous world, but now you will be able to use concurrently in millions of virtual threads.

As the article mentions, the whole reason why async/await was invented, is because doing concurrency with kernel threads is very expensive. They are slow to create and they use a lot of memory. Using Promises and an event loop you can run multiple concurrent operations in a single OS thread. This gives you the chance to achieve much more concurrency than running one operation per OS thread. The problem is that this changes the way you program a lot. Your code is not sequential anymore, your error handling is different, even some conditional operations are different. Async/await is just some syntactic sugar created to make this looks like the regular programming model. It makes the function calls look sequentials (by adding an `await` prefix), it makes the errors look like exceptions, it makes the for loops look like regular for loops. But this is just appearance. It's something that looks similar, but it's different, and that's why you can mix it with regular code.

Loom model is closer to Go and Erlang.

Re:State of Loom by Skinney on Saturday May 16, 2020 @09:53AM (#23203378)
Project Loom provides the same benefit of async/await, but without changing the syntax in any way. In C# you now have a split API: blocking api's which rely on threads for concurrency, and async api's which require you to use async/await syntax. These api's are not compatible, so to take advantage of async/await, you have to refactor your code.

When project loom is implemented in Java, all code which makes use of threads today automatically gains all the benefits of the async/await model, without any refactoring.

Go and Erlang doesn't do the async/await thing. They only have one notion of concurrency (goroutines in Go, processes in Erlang). They don't require you to use async and await constructs, but they work similarly to async/await under the hood.

Re:State of Loom by hn_throwaway_99 on Saturday May 16, 2020 @10:32AM (#23203664)
Couple specific examples you may be familiar with:

Coming from Java to JavaScript, I really, really yearn for the equivalent of ThreadLocals. For example, most Java logging libraries let you set a "context" object that can be used for logging a request ID or user ID anywhere down the call stack. You can't do that in Node, so instead you have to explicitly pass that context object down in all your function calls, or make use of brittle, complicated, hacky, not-fully-supported features like "continuation local storage".

Debugging stack traces in an async/await world is a mess because you don't get a nice "from the start of the request down to this call" view of the stack.

Perhaps most importantly, with Node at least you need to worry about starving your main event loop because there is only a single top-level main thread (but understand that's not inherent to the async/await issue in general).

Re:State of Loom by sk5t on Saturday May 16, 2020 @09:43PM (#23208749)
> Coming from Java to JavaScript, I really, really yearn for the equivalent of ThreadLocals

While I understand why you feel that way today, you may find yourself some point in the future deeply, deeply detesting ThreadLocal. Sure it seems all convenient but all hell breaks loose when you need to use a threadpool, a future, an actor model...

Re:State of Loom by int_19h on Saturday May 16, 2020 @10:07PM (#23208868)
This a Node limitation, though. E.g. .NET has had asynchronous call contexts, which encapsulate stuff such as "async thread locals", for a very long time.

Similarly, for the stacks, it's largely a tooling issue - if the debugger understands async, and the framework retains enough information about the origin of the promise, it can re-create the original stack trace, and combine it with the current one - .NET debuggers in VS and VSCode do this, as well, and WinRT even does this for C++.

Re:State of Loom by moogly on Sunday May 17, 2020 @07:40AM (#23211292)
Re:State of Loom by closeparen on Saturday May 16, 2020 @12:20PM (#23204745)
Code that runs in goroutines looks no different from code that runs outside goroutines. You do regular blocking IO. The application code of a performant RPC / HTTP server never needs to refer to any kind of concurrency construct, unless it wants to make outbound calls in parallel. (Your HTTP or RPC library takes care of dispatching inbound requests onto goroutines).

As much as I want a more expressive language, all the explicit async/await, futures, callbacks, etc. junk needed just for your API handler to cooperate with concurrent requests (not even do its own parallel processing) keeps on sending me back to Go. I look forward to being able to write Java in the same style.

Re:State of Loom by balfirevic on Saturday May 16, 2020 @11:11AM (#23204011)
> Can you maybe elaborate why this model is better than async/await?

Others have given actual explanations, but I'll chime in with a bit of a tongue-in-cheek one: it's better than async/await because it doesn't have async/await (yet has all the benefits provided by them).

Re:State of Loom by chrisweekly on Saturday May 16, 2020 @07:58PM (#23208223)
I wish I understood this better. If you're writing code in the same style whether it's async or not, how can you manage return values and reason about flow?
Re:State of Loom by balfirevic on Saturday May 16, 2020 @08:18PM (#23208306)
The flow is sequential, as laid out in the code, the same as for any old-fashioned code. It has the same semantics as the non-async code. The only difference is that OS thread can be released to do some other work while you wait for IO operation to finish.
Re:State of Loom by balfirevic on Saturday May 16, 2020 @09:31AM (#23203226)
I still have't fully made peace with the fact that C# choose async/await route :(

It could have been so much better.

Re:State of Loom by nlawalker on Saturday May 16, 2020 @04:30PM (#23206949)
Why didn’t C# or JS go this way?
Re:State of Loom by balfirevic on Saturday May 16, 2020 @05:23PM (#23207327)
For JS, cynical take would be that Javascript is just being consistent in making every wrong choice in language design. More serious (and more correct) answer is that, being single-threaded, adding any kind of threads would be pretty adventurous. Async/await as a sugar for promises is a decent improvement over using promises directly, so it's not so bad of a choice after all.

As for C#, some kind of fibers appear to have been considered but deemed too hard or not worth the trade-offs. See here: https://github.com/dotnet/runtime/issues/11084

The acceptable alternative was, apparently, forcing to programmers to litter their code with words such as async/await/task in-between the words and symbols that actually describe the purpose of their program, should they want to take advantage of non-blocking IO. All the way throughout the call-stack, nonetheless. And you also get crap like this: https://github.com/dotnet/corefx/pull/4868

Re:State of Loom by int_19h on Saturday May 16, 2020 @10:14PM (#23208897)
Win32 has fibers, which are essentially green threads. And for a while, .NET itself tried to support it. The problem is that there was never widespread support on the native side of things, and in .NET itself, user code has to not make certain assumptions to allow for fibers to work (e.g. that each managed thread maps 1:1 to an OS thread), which in practice nobody followed.

This points at the bigger problem that every implementation of green threads has: it breaks interop unless everybody buys into it. Async/await is fundamentally callback-based, which can be represented nicely even in C ABI - so literally any language that can interop with C, can interop with an async/await model, with execution flow asynchronous throughout the entire chain. Runtimes that choose to roll their own green threads, like Go, and I guess now also Java, are effectively stating that their convenience is worth the inability to interface well with async code outside of their runtime.

Re:State of Loom by balfirevic on Saturday May 16, 2020 @10:36PM (#23209028)
Win32 fibers would probably not be a good match for potential .NET fibers. From the link in my previous comment, they seem to allocate the stack of the same size as regular OS threads, making them not appropriate for the use case of keeping thousands or more fibers around.

> Runtimes that choose to roll their own green threads, like Go, and I guess now also Java, are effectively stating that their convenience is worth the inability to interface well with async code outside of their runtime.

You'd still be able to interop with async code outside of runtime, you can still model the API as callbacks for the interop purposes where needed.

But yes, interop has sam trade-offs. Which are, IMO, entirely correct trade-offs to make for Go, Java and .NET (and overwhelmingly so). But they are probably not for (as an example) Rust, according to it's goals.

Re:State of Loom by int_19h on Sunday May 17, 2020 @05:42AM (#23210857)
They're definitely not correct for .NET - you forget WinRT. That ecosystem was much more heavily invested in native interop from the get go - COM interop, P/Invoke, various runtime features like unmanaged pointers and unions etc. So once native async became a thing, .NET developers expect to be able to interop with it, as well.
Re:State of Loom by anonymoushn on Saturday May 16, 2020 @07:49AM (#23202658)
Will virtual threads be copyable?