← Back to context

Comment by rstuart4133

1 day ago

Ok, you've been programming for years. But didn't learn a lot about threads, apparently.

> Multithreaded code is often much harder to reason about than async code, because threads can interleave executions and threads can be preempted anywhere.

No, green threads / fibres or whatever you want to call them explicitly don't interleave executions. They are a form of cooperative multitasking. Async/await is another form of co-operative multitasking. One former just builds on what we already have. The latter re-invents the universe.

By the by, the blocker for Javascript green threads wasn't preemption, mostly because there isn't any. It's that Javascript has a "run to completion" model. If the DOM calls a javascript event (which is effectively how all javascript is invoked in a browser), it doesn't block, so it always runs to completion. Green threads break that model. It's not a insurmountable break - the DOM events could always still return immediately, but they could start a green thread that returns to them as soon as they block. Thinking about it, the change is possibly smaller than language changes required by async/await.

If you can reason about where an await is, you can reason about where a green thread yields. The only difference is that one of them clutters your syntax and the other doesn't.

> No, green threads / fibres or whatever you want to call them explicitly don't interleave executions.

If you use them with a multithreaded executor (eg in Go), of course they interleave executions. I suppose all your green threads / fibers could run on a single CPU core. But what's the point? How would that be an improvement over what we have now?

I suppose you could make something similar to async/await but with a yield() operation whenever a call wants to block. This would allow blocking read() and so on. But its basically async/await but without declaring functions as async. And without needing to explicitly await. Await points would be implicit and invisible. But if you do that, any function call you make could yield before returning. As a result, you could no longer easily reason about interleaving. I call foo(). Does it yield to other threads before returning? I have no idea. I could read the code of foo(), but maybe foo will change between minor versions of the library.

This would lead to an avalanche of bugs. Lots of javascript code quietly depends on the lack of interleaving for correctness. Javascript guarantees that while my (non async) function runs, no other code gets executed. Adding threads, even if its via cooperative multitasking, would break that invariant. It would break all sorts of programs which are working correctly today.

> The latter re-invents the universe. [...] Thinking about it, the change is possibly smaller than language changes required by async/await.

Did you write much javascript before async/await and before promises? Javascript at the time was already async. We just implemented async execution through callbacks. ("Callback hell"). Over time, functions tended to go down and to the right. Promises were added as 3rd party libraries. Then promises were standardised. And later, async/await was added as syntax to help you work with promises. Async / await in javascript was an incremental change to give us new syntax to do what we were already doing. JS already had an event loop and promises. Async/await just added syntax.

Threads (cooperative or preemptive) would be a massive change to JS. It would cause an endless parade of bugs, and frozen websites. To say nothing of your notion we could casually reinvent DOM events. That ship sailed a long time ago.

> The only difference is that one of them clutters your syntax and the other doesn't.

One of them is explicit about where and when a thread blocks. Whether or not something is "blocking" (async) is part of the API. Threading (incl cooperative threading) hides this information. Personally, I much prefer this information to be explicit. I need to know as a programmer whether or not execution will be interleaved.

  • > I suppose all your green threads / fibers could run on a single CPU core.

    yes.

    > But what's the point? How would that be an improvement over what we have now?

    > But its basically async/await but without declaring functions as async.

    You answered your own question: yes, you get what you have now, without all the overhead of async, await, promises and futures.

    > But if you do that, any function call you make could yield before returning.

    A green thread could be an instance of a particular type, so `input = self.yield()` would fail if you aren't a green thread. So no, not "any function" - just ones that instances of a green thread, or are passed a reference to one.

    > Does it yield to other threads before returning?

    It could if you pass it an instance to a green thread, otherwise it can't.

    > This would lead to an avalanche of bugs.

    It doesn't. Cooperative multitasking is at least 1/2 a century old at this point. The bugs you're imagining will happen mostly aren't an issue. To the extent they do happen, it's because someone hasn't thought about two control flows modifying the same data structure. Yes, that happens, but it happens in all single threaded code - async included. It's why we hate side effects. It's what Rust famously prevents with its borrow checker even in the face of side effects. It's not avoided by async. The explicit colouring does not help to prevent it - it's just overhead.

    FWIW the one issue cooperative multitasking does often introduce is that they can take a long time to execute, so other cooperative tasks don't run in a timely fashion. Exactly the same thing can happen with async of course. It's not usually a problem in browsers, but in embedded solutions where cooperative multitasking is commonly used, it's a real issue because they are often real time. Ask me how I know.

    > Javascript guarantees that while my (non async) function runs, no other code gets executed.

    This remains true. You are getting confused by your mental model of threads as a form of concurrency. There is no concurrency going on there. Semantically it is near identical to async / await. The principle difference is in async / await, the program is explicitly creating each stack frame on the heap using manually allocated objects. In addition to the mental overhead that creates, it slower than using a real stack like green threads do. But now for the truly bizarre twist. Can you guess how modern javascript engines get around that speed issue? Wait for it .... they create an explicit stack ... that looks like what green threads would use anyway! And as a wonderful side effect - you get real stack back traces again. The irony is almost palpable. https://v8.dev/blog/fast-async

    > Threads (cooperative or preemptive) would be a massive change to JS. It would cause an endless parade of bugs, and frozen websites. To say nothing of your notion we could casually reinvent DOM events. That ship sailed a long time ago.

    I agree the ship has sailed at this point. The rest of the assertions you make there are wrong.

    This assertion stands out: frozen websites. Can you tell me how they are going to block? There are no blocking calls in javascript now. The things you would await on now would be passed a green thread handle. But the javascript scripts events called from the DOM have no green-thread handle, so they can't block.

    > Personally, I much prefer this information to be explicit. I need to know as a programmer whether or not execution will be interleaved.

    You don't. You've just been conditioned to think that because you've never done it any other way. But the reality is people have been using cooperative multitasking for a long, long time. It pre-dates threads and async. The issues and bugs you are proclaiming would happen don't arise.

    • If we invented a new language, sure. Cooperative multitasking might be a fun approach. The avalanche of bugs I’m imagining would come from existing JavaScript code being run in a different context than that in which it was written and tested. If you pass me a callback right now, and I call a(); callback(); b();. I can guarantee that the program doesn’t yield to the event loop or other executions between a() and b(). As I understand it, this guarantee no longer holds with coop. multitasking because your callback can yield to another thread.

      Good on the V8 team. Sounds like they’ve figured out a way to get the performance of green threads with the better ergonomics of effects systems (async await). Great!

      You sound like an expert in cooperative multithreading. If async await can use real stacks, what actual benefits are there to cooperative multithreading? Why prefer them over what JS has now? Pitch them to me.

      4 replies →