hvrosen
03/16/2022, 9:24 AMsync
and async
are so fundamentally different in their semantics. What it that difference was present on the language level, in stead of being hidden by magic?
This has been done many decades ago and with superb usability results, in the form of state machines and most notably statecharts. “Red” (sync) functions are transitions. “Blue” (async) functions are states. Thinking of them and visualizing them as distinctly different is a game-changer for the human mind: A top-down understanding can be conveyed to non-programmers and programmers alike, etc. In the typical visualization, sync stuff is not “red functions”, they are arrows. async stuff is not “blue functions”, they are rectangles. Arrows and Rectangles composes perfectly, because of their differences. It’s hard to see what’s gained conceptually by merging arrows and rectangles into one thing.Mariano Guerra
Ray Imber
03/18/2022, 6:17 PMJimmy Miller
fetch-the-data
isn't a state. In fact, I could make many many state transitions while my async function is still resolving.
To be pedantic, Go has a cooperative scheduler.
But to go beyond the pedanticness, I don’t think actors or CSP solve the colored functions problem. There are still colors here, just not functions. Sending a message to an actor or putting a message on a channel are fundamentally different operations. You can’t parameterize on them.
That said, I think that coloring functions is actually quite good. There are so many unique aspects to asynchronous programming that I’ve seen so many people get wrong. I have spent weeks/months of time trying to help a QA team whose framework hid the fact that asynchrony was going on from them. They constantly ran up against race conditions. To the point where there were 8 people, whose full time jobs was to run tests on their personal laptops, because CI was “too slow” and “caused errors”. They even blamed the errors in their test suites on the app having a “memory leak”.
Needless to say, I'm all for these things being very explicit, because hiding them, or abstracting away these details leads to very subtle and confusing bugsSteve Dekorte
03/21/2022, 6:06 PMJimmy Miller
cooperative multi-stack, async i/oWhat language/system would be a good example here?
Steve Dekorte
03/21/2022, 6:45 PMJimmy Miller
Steve Dekorte
03/23/2022, 7:53 PMNaveen Michaud-Agrawal
03/24/2022, 2:51 AMJimmy Miller
Steve Dekorte
03/25/2022, 5:31 PMRay Imber
03/25/2022, 7:05 PMSteve Dekorte
03/25/2022, 11:48 PMSteve Dekorte
03/25/2022, 11:50 PMRay Imber
03/26/2022, 1:08 AMSteve Dekorte
03/28/2022, 6:14 PMRay Imber
03/28/2022, 7:18 PMIt's like automatic garbage collection. The point isn't that memory management isn't done, it's that it's done for you.I don't think it's the difference between manual memory management and GC, it's the difference between reference counting and GC. Both pre-emptive multitasking and co-routines are automatic scheduling algorithms. Neither requires you to build the state machine yourself. The difference is how you influence the scheduling. Pre-emptive schedulers don't take any influence from your program. Whereas coroutines provide "yield" points that can be seen as hints to the scheduler to be more in line with the needs of your program.
is like programming with GOTOs instead of using functionsI don't think that's accurate at all. Are you saying all state machines can be represented as coroutines? This duality seems much more like the Church-Turing duality of lambda calculus vs. Turing machines. It's theoretically important but not practical by itself. I'm also not convinced your argument is true if that's the case. Coroutines deal with a particular class of state machines (those dealing with scheduling), they are not appropriate for all state machines. Are you saying that all state machines are better represented as cooroutines? If that's the case, then this is very clearly subjective. That is an argument similar to the difference between functional and imperative programming. There is no evidence that one is universally better than another. Some things are better represented functionally, and some better procedurally. Anecdotally, I dislike the GOTO argument. It misses too much nuance, in the same way that "functional programming is always better" is a useless statement. Both Knuth and Dijkstra himself walked back the "GOTO considered harmful" statements.
Donald E. Knuth: I believe that by presenting such a view I am not in fact disagreeing sharply with Dijkstra's ideas, since he recently wrote the following: "Please don't fall into the trap of believing that I am terribly dogmatical about [the go to statement]. I have the uncomfortable feeling that others are making a religion out of it, as if the conceptual problems of programming could be solved by a single trick, by a simple form of coding discipline!https://pic.plover.com/knuth-GOTO.pdf Programming abstractions should be ladder that you can both walk up or down depending on the engineering needs of the particular problem. Dogma has no place in engineering imo. To be fair, we are all human, so that is more of an aspiration than the reality.
Ray Imber
03/28/2022, 7:35 PMcoroutines allow us to avoid the complexity of state machines (by using stacks) without making in impossible to write correct code (as preemption does).
...
This is unlike multiple threads preemptively writing on the same memory at the same time, which is an environment where even the world's top experts have been shown to be unable to write correct code.Sorry, I don't buy it. Async lets you write more efficient code around IO scheduling. It solves a particular problem, that is completely orthogonal to correctness or memory safety. Coroutines are about "when", not "where". Corountines do not solve multithreaded memory safety. Async and Parallelism are not the same thing. You can still have race conditions with coroutines (Go has had plenty of bugs showing this). Multiple Coroutines can still be scheduled on different CPU cores and write to the same memory. You need something like the Rust borrow checker to solve that problem. Not coroutines.
Coroutines also have the advantage (depending on how they are implemented) of having far smaller (and extendable) stacks than preemptive threads, which allows them to scale to several orders of magnitude more concurrency than preemptive/OS threads.This is the problem coroutines solve. Coroutines deal with optimization, not correctness.
Steve Dekorte
03/28/2022, 7:39 PMRay Imber
03/28/2022, 7:45 PMSteve Dekorte
03/28/2022, 8:22 PMRay Imber
03/28/2022, 8:36 PMRay Imber
03/28/2022, 8:46 PMRay Imber
03/28/2022, 9:33 PMRay Imber
03/28/2022, 9:42 PMRay Imber
03/28/2022, 9:55 PMSteve Dekorte
03/28/2022, 10:00 PMRay Imber
03/28/2022, 10:25 PMThis is where the “red functions can only be called by red functions” rule comes from. You have to closurify the entire callstack all the way back toWhat's the difference between "closurify the entire callstack" and "reified callstacks"? An implementation detail, that's what. They are semantically equivalent. You implement a coroutine scheduler using a trampoline at the main and a set of execution stacks.or the event handler.main()
Go is the language that does this most beautifully in my opinion. As soon as you do any IO operation, it just parks that goroutine and resumes any other ones that aren’t blocked on IO.You know that "goroutines" are just a fancy closure right? When you write the go program "read("file.txt")", it's essentially tranformed into "await read("file.txt")"
Steve Dekorte
03/28/2022, 10:37 PMRay Imber
03/28/2022, 10:37 PMThe point here is that thefunction, which is the demo of typical async/await usage, works in both an async context and blocking context. The programmer was able to express the inherent parallelism of the logic, without resorting to function coloring.amain
There is admittedly a bit of boilerplate in the example. Here's the tracking issue for that.
Now for the related Standard Library updates:
This introduces the concept of "IO mode" which is configurable by the Root Source File (e.g. next toI personally think the Zig solution is much better than the Go solution.pub
fn
). Applications can put this in their root source file:main
Ray Imber
03/28/2022, 10:55 PMhere is a difference between what is possible and what is practical. For example, if I try to use some open source JS code, will everything be written in async/await? Or will I have to rewrite it and potentially maintain the async/await version will future updates of the module? What about all of its dependencies? Do I update and maintain all of those too?That's a good argument, though as much of a political one as a technical one. It's more practical from a technical standpoint than you might think though. It's possible at the compiler level to use program transforms. i.e. lisp style macros (what Nim and lisp do) or compiler passes (what Zig and Go do) to automate a lot of that, at least from the point of the standard library. Just make the standard library I/O calls async, and automatically transform the callers into delimited continuation closures. Admittedly that is fairly invasive and not foolproof (depending on how the caller code was written).
Steve Dekorte
03/28/2022, 11:36 PMRay Imber
03/29/2022, 12:35 AMRay Imber
03/29/2022, 12:40 AMRay Imber
03/29/2022, 1:20 AMSteve Dekorte
04/17/2022, 9:14 PMSteve Dekorte
04/17/2022, 9:16 PMRay Imber
04/17/2022, 9:38 PMSteve Dekorte
04/17/2022, 9:42 PMRay Imber
04/17/2022, 10:02 PMSteve Dekorte
04/18/2022, 5:26 PMSteve Dekorte
04/18/2022, 5:33 PMRay Imber
04/18/2022, 6:04 PMcontext switching is limited to all call boundaries instead of individual instructionsIsn't that what cooperative threading is? You are telling the scheduler when it is allowed to pre-empt. It cannot pre-empt arbitrarily. The actual scheduling order is abstracted and doesn't matter to your user code. You yourself mentioned in an earlier comment that the scheduler can be abstracted from the user.
I don't see why your coroutine scheduling code can't provide ways for you to do whatever you like. Many languages with coroutines (e.g. Lua) leave scheduling up to the programmer. I think this is a mistake though, as modules from different programmers can be effectively incompatible without a shared scheduler.Pre-emption itself isn't the problem, it's when pre-emption is allowed to happen that makes things difficult. Also, I want to make very clear, that threads have nothing to do with async or cooroutines. Concurrency is not parallelism. Cooroutines still work with a single thread. I think you are conflating the abstraction with the implementation here. Unless you are working on an embedded system with no operating system, you will always have some level of pre-emption from the OS. This is the foundation of modern timesharing computing. You seem to assume that a "proper" cooroutine implementation has no pre-emption at all, but that is not the case. Have you seen this series of articles that describes how go routines are implemented? It's a really good deep dive into the architecture of cooroutines. It goes into good detail about how Go interacts with the OS preemptive scheduler. https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html Regarding JS, iterators can be used to implement the runtime scheduler. This works by having a queue of async 'thunks' or promises (units of work). The iterator then pops those off the queue and runs them. The iterator can know which promise chain something came from and can schedule appropriately. The user can even influence this depending on the implementation.
Steve Dekorte
04/18/2022, 6:12 PMRay Imber
04/18/2022, 6:18 PMSteve Dekorte
04/19/2022, 12:28 AMRay Imber
04/19/2022, 5:42 PMRay Imber
05/13/2022, 5:02 PMSteve Dekorte
05/14/2022, 6:38 PM