17 ms·
Playground Wisdom: Threads Beat Async/Await
- NeutralForest 2y agoI thought that was interesting and I definitely get the frustration in some aspect. I'm mostly familiar with Python and the function "coloring" issue is so annoying as it forces you to have two APIs depending on async or not (look at SQLAlchemy for example). The ergonomics are bad in general and I don't really like having to deal with, for example, awaiting for a result that will be needed in a sync function. That being said, some alternatives were mentioned (structured concurrency à la Go) but I'd like to hear about people in BEAM land (Elixir) and what they think about it. Though I understand that for system languages, handling concurrency through a VM is not an option.
- the_mitsuhiko 2y ago> structured concurrency à la Go Go does not have structured concurrency. Goroutines as far as I know don't have much of a relationship with each other at all.
- NeutralForest 2y agoMy bad, thanks for the correction.
- lbrindze 2y agoBEAM very much falls into the same camp as the author's description of Scratch does at the beginning of the article. You have a lot more granular control than Scratch, of course, but it also loosely follows the actor model
- toast0 2y agoSorry, I kind of spaced on reading the article, but from BEAM land, everything is built around concurrent processes with asynchronous messaging. You can send a message to something else and wait for the response immediately if you want to write in a more blocking style. And you can write a function that does bot the sending a message and the waiting, so you don't really need to think about it if you don't want to. All I/O pretty much feels the same way, although you get into back pressure with some I/O where if there's a queue you can opt to fail immediately or block your process until the send fits in the queue. The underlying reality is that your processes don't actually block, BEAM processes are essentialy green threads that are executed by a scheduler (which is an OS thread), so blocking things become yields, and the VM also checks if it should yield at every function call. BEAM is built around functional languages, so it lacks loops and looping is handled by recursive function calls, so a process must make a function call in a finite amount of code, and so BEAM's green threading is effectively pre-emptive. The end result of all this is you can spawn as many processes as you like (i've operated systems with one process per client connection, and millions of client connections per node). And you can write most of your code like normal imperitive blocking code. Sometimes you do want to separate out sending messages and receiving responses, and you can easily do that too. This is way nicer than languages with async/await, IMHO; there's no trickyness where calling a blocking function from a async context breaks scheduling, and calling an async function from a non-async context may not be possible... You do still have the possibility of a function blocking when you didn't expect it to, but it will only block the process that called it and transitively, those processes that are waiting for messages from the now blocked process. Java's Project Loom seems like it will get to a pretty similar place, eventually. But I've seen articles about some hurdles on the way; there's some things that still actually block a thread rather than being (magically) changed to yielding. Again, IMHO, people didn't build async/await because it is good. They built it because threads were unavailable (Javascript) or to work around the inability to run as many threads as would make the code simple. If you could spawn a million OS threads without worrying about resource use, only constrained languages would have async/await. But OS threads are too heavy to spawn so many, and too heavy to regularly spawn and let die for ephemeral tasks.
- vacuity 2y agoTo offer a different perspective, green threads are far easier to use for programmers (which is a valuable quality; I'm not dismissing that), but async/await is the better abstraction if you know how to use it. I justify this more in [0]. Async/await is great, but pragmatically it should not be pushed as the sole "asynchronous programming" abstraction. It requires more QoL infrastructure to be even somewhat usable as general-purpose, although I think it'll be worthwhile to continue bridging the gap. [0] https://news.ycombinator.com/item?id=41900215 https://news.ycombinator.com/item?id=41900215
- arctek 2y agoI actually think out of any language async/await makes the most sense for javascript. In the first example: there is no such thing as a blocking sleep in javascript. What people use as sleep is just a promise wrapper around a setTimeout call. setTimeout has always created microtasks, so calling a sleep inline would do nothing to halt execution. I do agree that dangling Promises are annoying and Promise.race is especially bad as it doesn't do what you expect: finish the fastest promise and cancel the other. It will actually eventually resolve both but you will only get one result. Realistically in JS you write your long running async functions to take an AbortController wrapper that also provides a sleep function, then in your outer loop you check the signal isn't aborted and the wrapper class also handles calling clearTimeout on wrapped sleep functions to stop sleeping/pending setTimeouts and exit your loop/function.
- tempodox 2y ago> async/await makes the most sense for javascript. More like: Has no alternative. There are no threads in JS.
- threadthis 2y agoYes there are. They are called "Workers" (WebWorkers), though. It's a semantic game with a different API but the same concept. https://www.w3.org/TR/2021/NOTE-workers-20210128/ https://www.w3.org/TR/2021/NOTE-workers-20210128/
- com2kid 2y agoNote that the underlying C++ libraries for Node are perfectly capable of using threads, only the final user space model is single threaded by default.
- huem0n 2y ago_ function blockingSleep(amt) { let start = new Date().getTime() while (start + amt > new Date().getTime()) {} }
- 2y ago
- serbuvlad 2y agoAs someone who has only written serious applications in single-threaded, or manually threaded C/C++, and concurrent applications in go using goroutines, channels, and all that fun stuff, I always find the discussion around async/await fascinating. Especially since it seems to be so ubiquitous in modern programming, outside of my sphere. But one thing is: I don't get it. Why can't I await in a normal function? await sounds blocking. If async functions return promises, why can't I launch multiple async functions, then await on each of them, in a non-async function that does not return a promise? I get there are answers to my questions. I get await means "yeald if not ready" and if the function is not async "yeald" is meaningless. But I find it a very strange way of thinking nonetheless.
- avandekleut 2y agoAt least in node, its because the runtime is an event loop.
- mrkeen 2y agoYou can: https://hackage.haskell.org/package/async-2.2.5/docs/Control-Concurrent-Async.html#g:4 https://hackage.haskell.org/package/async-2.2.5/docs/Control... As long as you don't mind - what did the article say? - >> transcending to a higher plane and looking down to the folks who are stitching together if statements, for loops, make side effects everywhere, and are doing highly inappropriate things with IO.
- do_not_redeem 2y ago[flagged]
- Const-me 2y agoThe article seems specific to JavaScript, C# is different. > you cannot await in a sync function In C# it’s easy to block the current thread waiting for an async task to complete, see Task.Wait method. > since it will never resolve, you can also never await it In C#, awaiting for things which never complete is not that bad, the standard library has Task.WhenAny() method for that. > let's talk about C#. Here the origin story is once again entirely different Originally, NT kernel was designed for SMP from the ground up, supports asynchronous operations on handles like files and sockets, and since NT 3.5 the kernel includes support for thread pool to dispatch IO completions: https://en.wikipedia.org/wiki/Input/output_completion_port https://en.wikipedia.org/wiki/Input/output_completion_port Overlapped I/O and especially IOCP are hard to use directly. When Microsoft designed initial version of .NET, they implemented thread pool and IOCP inside the runtime, and exposed higher-level APIs to use them. Stuff like Stream.BeginRead / Stream.EndRead available since .NET 1.1 in 2003, the design pattern is called Asynchronous Programming Model (APM). Async/await language feature introduced in .NET 4.5 in 2012 is a thin layer of sugar on top of these begin/end asynchronous APIs which were always there. BTW, if you have a pair of begin/end methods, converting into async/await takes 1 line of code, see TaskFactory.FromAsync.
- zamadatix 2y agoTask.Wait() is just using the normal "thread" (in the way the author defines it later) blocking logic to do that in said case but I think the author is trying to talk about pure async/await approaches there as an example of why you still want exactly that kind of non-async "thread" blocking to fall back on for differently colored functions. Task.WhenAny() is similar to Promise.any()/Promise.race(). I'm not sure this is where the author is focusing attention on though. Regardless if your execution is able to move on and out of that scope those other promises may still never finish or get cleaned up.
- the_mitsuhiko 2y agoYou're probably right that this is leaning in on JavaScript and Python more, but I did try to make a point that the origin story for this feature is quite a bit different between languages. C# is the originator of that feature, but the implications of that feature in C# are quite different than in for instance JavaScript or Python. But when people have a discussion about async/await it often loses these nuances very quickly. > Async/await language feature introduced in .NET 4.5 in 2012 is a thin layer of sugar on top of these begin/end asynchronous APIs which were always there. You are absolutely right. That said, it was a conscious decision to keep the callback model and provide "syntactic sugar" on top of it to make it work. That is not the only model that could have been chosen.
- RantyDave 2y agoAlmost as an aside the article makes an interesting point: memory accesses can block. Presumably if it blocks because it's accessing a piece of hardware the operating system schedules another thread on that core ... but what if it blocks on a 'normal' memory access? Does it stall the core entirely? Can 'hyperthreading' briefly run another thread? Does out of order execution make it suddenly not a problem? Surely it doesn't go all the way down to the OS?
- the_mitsuhiko 2y ago> but what if it blocks on a 'normal' memory access? Does it stall the core entirely? You won't be able to suspend a virtual thread, so that OS thread is going to be blocked no matter what. As far as kernel threads are concerned I think in practice when a page fault happens the kernel yields and lets another thread take over.
- anonymoushn 2y agoHyperthreading is a feature where a single core can process two unrelated instruction streams (i.e. two threads) which is useful for software that executes few instructions per cycle.
- magicalhippo 2y ago> what if it blocks on a 'normal' memory access? If the CPU gotta wait for memory it's gotta wait, and so it just won't make progress. Though we typically say that the CPU has stalled. How long depends on if it's found in one of the caches, they're progressively slower, or main memory. All the fancy techniques like out of order execution, speculative execution and hyperthreads are mainly there to trigger memory reads as soon as possible to reduce how long it is stalled. Some nice detailed SE answer here[1] with some details. [1]: https://electronics.stackexchange.com/a/622912 https://electronics.stackexchange.com/a/622912
- unscaled 2y agoI think most of the arguments in this essay rely on this single premise: "The second thing I want you to take away is that imperative languages are not inferior to functional ones." There is an implied assumption that async/await is a "functional feature" that was pushed into a bunch of innocent imperative languages and polluted them. But there is one giant problem with this assumption: async/await is not a functional feature. If anything, it's the epitome of an imperative flow-control feature. There are many kinds of functional languages out there, but I think the best common denominator for a primarily functional language nowadays is exactly this: in functional languages control flow structures are first class citizens, and they can be customized by the programmer. In fact, most control flow structures are basically just functions, and the one that aren't (e.g. pattern matching in ML-like languages and monadic comprehensions in Haskell-inspired languages) are extremely generic, and their behavior depends on the types you feed into them. There are other emphasis points that you see in particular families of languages such as pattern matching, strict data immutability or lazy computation — but none of these is a core functional concept. The interesting point I want to point out is that no primarily functional language that I know actually has async/await. Some of them have monads and these monads could be used for something like async/await but that's not a very common use, and monad comprehensions can be used for other things. For instance, you could use do expressions in Haskell (or for expressions in Scala) to operate on multiple lists at once. The same behavior is possible with nested for-loops in virtually every modern imperative language, but nobody has blamed Algol for "polluting" the purity of our Fortran gotos and arithmetic ifs with this "fancy functional garbage monad from damned ivory tower Academia". That would be absurd, not only because no programming language with monadic comprehensions existed back then, but also because for loops are a very syntax for a very specific thing that can be done with monadic expression. They turn a very abstract functional concept into a highly specific — and highly imperative — feature. The same is true for await. It's an imperative construct that instructs the runtime to suspend (or the compiler to turn the current function into a state machine). So no, async/await does not have anything to do with functional-language envy and is, in fact, a feature that is quite antithetical to functional programming. If there is any theoretical paradigm behind async/await (vs. just using green threads), it's strong typing and especially the idea of representing effects by types. This is somewhat close to fully-fledged Effect Systems (in languages such as a Koka), but not as powerful. The general idea is that certain functions behave in a way that is "infective" — in other words, if foo() calls bar() which in-turn calls doStuff(), it might be impacted by some side-effect of doStuff(). In order to prevent unpleasant surprises, we want to mark this thing that doStuff does in the function signature (either using an extra argument, a return type wrapper or just an extra modifier like "async"). In a pure language like Haskell, everything from I/O to mutable memory requires specifying an effect and this is usually done through monadic return types. But even the very first version of Java (Ron Pressler's ideal untarnished "imperative" language) has effects (or "colors") which still remain in the language: checked exceptions. They are just as infective as async I/O. If you don't handle exceptions in place, a function marked with "throws IOException" (basically almost any function that deals with I/O) can only be called by another function marked with "throws IOException". What's worse, unlike JavaScript which only has two colors (async and non-async), Java has an infinite number colors! The description above sounds horrible, but it's not. Checked exceptions are widely believed to be a mistake[1], but they don't bother Java developers enough to make the language unusable. You can always just wrap them with another exception and rethrow. The ergonomics could have been made slightly better, but they're decent enough. But the same can be said for async/await. If you take a language with a similar feature that is close to Java (C# or Kotlin), you'll see the asynchronous functions can still run as blocking code from inside synchronous functions, while synchronous functions can be scheduled on another thread from a synchronous function. The ergonomics for doing that are not any harder than wrapping exceptions. In addition to that, the advantages of marking a function that runs asynchronous I/O (just like marking a function that throws an exception) are obvious, even if the move itself is controversial. These functions generally involve potentially slow network I/O and you don't want to call them by mistake. If you think that never happens, here is the standard Java API for constructing an InetAddress object from a string representing an IPv4 or IPv6 address: InetAddress.getByName()[2]. Unfortunately, if your IP address is invalid, this function may block while trying to resolve it as a domain name. That's plain bad API design, but APIs that can block in surprising ways are abundant, so you cannot argue that async/await doesn't introduce additional safety. But let's face it — in most cases choosing async/await vs. green threads for an imperative language is a matter of getting the right trade-off. Async/Await schedulers are easier to implement (they don't need to deal with segmented/relocatable/growable stacks) and do not require runtime support. Async/await also exhibits more efficient memory usage, and arguably better performance in scenarios that do not involve a long call-graph of async functions. Async/await schedulers also integrates more nicely with blocking native code that is used as a library (i.e. C/C++, Objective C or Rust code). With green threads, you just cannot run this code directly from the virtual thread and if the code is blocking, your life becomes even harder (especially if you don't have access to kernel threads). Even with full control of the runtime, you'd usually end up with a certain amount of overhead for native calls[3]. Considering these trade-offs, async/await is perfect in scenarios like below: 1. JavaScript had multiple implementations. Not only were most of them single-threaded, they would also need a major overhaul to support virtual threads even if a thread API was specified. 2. Rust actually tried green threads and abandoned them. The performance was abysmal for a language that seeks zero-cost abstraction and the system programming requirements for Rust made them a deal breaker even if this wasn't the case. Rust just had to support pluggable runtimes and mandating dynamic stacks just won't work inside the Kernel or in soft real-time systems. 3. Swift had to interoperate with a large amount of Objective C called that was already using callbacks for asynchronous I/O (this is what they had). In addition, it is not garbage-collected language, and it still needed to call a lot of C and Objective C APIs, even if that was wrapped by nice Swift classes. 4. C# already had a Promise-like Task mechanism that evolved around wrapping native windows asynchronous I/O. If .Net was redesigned from scratch nowadays, they could have very well went with green threads, but the way .Net developed, this would have just introduced a lot of compatibility issues for almost no gains. 5. Python had the GIL, as the article already mentioned. But even with patching runtime I/O functions (like greenlet — or more accurately, gevent[4] — did), there were many third party libraries relying on native code. Python just went with the more compatible approach. 6. Java did not have any established standard for asynchronous I/O. CompletableFuture was introuced in Java 8, but it wasn't as widely adopted (especially in the standard library) as the C# Task was. Java also had gauranteed full control of the runtime (unlike JavaScript and Rust), it was garbage collected (unlike Rust and Swift) and it had less reliance on native code than Swift, Pre-.NET Core C# or Python. On the other hand, Java had a lot of crucial blocking APIs that haven't been updated to use CompletableFuture, like JDBC and Servlet (Async Servlets were cumbersome and never caught on). Introducing async/await to Java would mean having to rewrite or significantly refactor all existing frameworks in order to support them. That was not a very palatable choice, so again, Java did the correct thing and went with virtual threads. If you look at all of these use cases, you'd see all of these languages seem to have made the right pragmatic choice. Unless you are designing a new language from scratch (and that language is garbage collected and doesn't need to be compatible with another language or deal with a lot of existing native code), you can go with the ideological argument of "I want my function to be colorless" (or, inversely, you can go with the ideological argument of "I want all suspending functions to be marked explicitly"). In all other cases, pragmatism should win. --- [1] Although it mostly comes to bad composability — checked result types work very well in Rust. [2] https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/InetAddress.html#getByName(java.lang.String) https://docs.oracle.com/en/java/javase/17/docs/api/java.base... [3] See the article blelow for the overhead in Go. Keep in mind that the Go team has put a lot of effort into optimizing Cgo calls and reducing this overhead, but they still cannot eliminate it entirely. https://shane.ai/posts/cgo-performance-in-go1.21/ https://shane.ai/posts/cgo-performance-in-go1.21/ [4] https://www.gevent.org/ https://www.gevent.org/
- andrewstuart 2y agoFeels academic because despite the concerns raised, I only experience async/await as a good thing in real world.
- BugsJustFindMe 2y agoI don't. Now what? I agree with the author, especially in Python. The core Python developers so lost their minds fleeing from the GIL that they forgot historical lessons about how much more ergonomic preemptive multitasking is vs cooperative.
- smitty1e 2y ago> so lost their minds fleeing from the GIL that they forgot historical lessons I just don't agree. `async def` gets the fact that we've departed Kansas, good Toto, right out front. Async moves the coder a step in the direction of the operating system itself. The juice is not worth the squeeze unless the project bears fruit. I hardly do enough networky stuff to make this facility useful to me, but I'm grateful to the hours poured into making it part of Python. Contra the author of The Famous Article, threads seem gnarlier still, and I would likely architect out any async parts of a system into another service and talk to it over a port.
- BugsJustFindMe 2y ago> I would likely architect out any async parts of a system into another service and talk to it over a port This doesn't get you any closer to the goal though. Would you talk to the other service with a blocking socket request or a non-blocking one? A non-blocking socket request either invokes a system thread or asyncio. You can't escape that part.
- whoisthemachine 2y ago> Your Child Loves Actor Frameworks It turns out, Promises are actors. Very simple actors that can have one and only one message that upon resolution they dispatch to all other subscribed actors [0]. So children might love Promises and async/await then? Personally, I've often thought the resolution to the "color" debate would be for a new language to make all public interfaces between modules "Promises" by default. Then the default assumption is "if I call this public function it could take some time to complete". Everything acting synchronously should be an implementation detail that is nice if it works out. https://en.wikipedia.org/wiki/Futures_and_promises#Semantics_of_futures_in_the_actor_model https://en.wikipedia.org/wiki/Futures_and_promises#Semantics...
- emadda 2y agoThat's a nice mental model for promises. But it is not always true that one promise instance can be awaited in multiple places. In Swift you cannot get the ref to the Promise instance, so you cannot store it or await it at multiple places. Once you start an async fn the compiler forces you to await it where it was started (you can use `await task.value`, but that is a getter fn that creates a new hidden promise ref on every call).
- whoisthemachine 2y agoI'm not familiar with Swift, but it still sounds like it's describing an actor model, just one with a subset of the functionality.
- manmal 2y agoIf I understand the use case correctly, then, in Swift, Tasks provide exactly what you have described. You can use the Task object in multiple places to await the result.
- marcosdumay 2y ago> Personally, I've often thought the resolution to the "color" debate would be for a new language to make all public interfaces between modules "Promises" by default. Works well on Haskell. You can even remove that "public" and "between modules".
- cryptonector 2y agoThreads are definitely not _the_ answer but _an_ answer. You can have as many threads as hardware threads, but in each thread you want continuation passing style (CPS) or async-await (which is a lot like syntactic sugar for CPS). Why? Because threads let you smear program state over a large stack, increasing memory footprint, while CPS / async-await forces you to make all the state explicit and compressed, thus optimizing memory footprint. This is not a small thing. If you have thread-per-client services, each thread will need a sizeable stack, each stack with a guard page -- even with virtual memory that's expensive, both to set up and in terms of total memory footprint. Between memory per client, L1/L2 cache footprint per client, page faults (to grow the stack), and context switching overhead, thread-per-client is much more expensive than NPROC threads doing CPS or async-await. If you compress the program state per client you can fit more clients in the same amount of memory, and the overhead of switching from one client to another is lower, thus you can have more clients. This is the reason that async I/O is the key to solving the "C10K" problem: it forces the programmer to compress per-client program state. But if you don't need to cater to C10K (or C10M) then thread-per-client is definitely simpler. So IMO it's really about trade-offs. Does your service need to be C10K? How much are you paying for the hardware/cloud you're running it on? And so on. Being more efficient will be more costly in developer cycles -- that can be very expensive, and that's the reason that research into async-await is ongoing: hopefully it can make C10K dev cheaper. But remember, rewrites cost even more than doing it right the first time.
- bvrmn 2y ago> Does your service need to be C10K? It's incorrect question. The correct one "Do your downstream services could handle C10K?" For example a service with a database should almost never be bothered with C10K problem unless most of the requests could skip db access. Every time you introduce backpressure handling in C10K-ready app it's a red flag you should simply use threads.
- cryptonector 2y agoI think you're saying that a database can't be C10K. Why? You don't say but I imagine that you mean because it's I/O bound, not CPU bound. And that may be true, but it may also not be true. Consider an all in-memory database (no paging): it will not be I/O bound. > Every time you introduce backpressure handling in C10K-ready app it's a red flag you should simply use threads. That's an admission that threads are slower. I don't see why you wouldn't want ways to express backpressure. You need backpressure for when you have impedance mismatches in performance capabilities; making all parts of your system equally slow instead is not an option.
- mark_l_watson 2y agoNice read, and the article got me to take a look at Java’s project Loom and then Eric Normand’s writeup on Loom and threading options for Clojure. Good stuff.
- pwdisswordfishz 2y ago> Go, for instance, gets away without most of this, and that does not make it an inferior language! Yes, it does. Among other things.
- manmal 2y agoI view Swift‘s Tasks as a thread-like abstraction that does what the author is asking for. Not every Task is providing structured concurrency in the strict sense, because cancellation has to be managed explicitly for the default Task constructor. But Tasks have a defined runtime, cancellation, and error propagation, if one chooses to use a TaskGroup, async let, or adds some glue code. The tools to achieve this are all there.
- FlyingSnake 2y agoI expected to see Swift but seems like most such discussions overlook it. Here’s a great discussion that goes deeper into it: https://forums.swift.org/t/concurrency-structured-concurrency/41622 https://forums.swift.org/t/concurrency-structured-concurrenc...
- agentkilo 2y agoPeople should try Janet (the programming language). Its fiber abstraction got everything right IMO. Functions in Janet don't have "colors", since fiber scheduling is built-in to the runtime in a lower level. You can call "async" functions from anywhere, and Janet's event loop would handle it for you. It's so ergonomic that it almost feels like Erlang. Janet has something akin to Erlang's supervisor pattern too, which, IMO, is a decent implementation of "structured concurrency" mentioned in the article.
- nextcaller 2y agoI'm still not sure if function coloring is also a problem in javascript. The problem became very clear in other languages like python or c#. But in javascript i've been writing code only awaiting a function when I need to and haven't ran into issues. I might write some simple experiment to check myself.
- neonsunset 2y agoWhy is it not a problem in JavaScript but is one in C#?
- nextcaller 2y agoIn c#/python you are forced to await the whole chain, the compiler forces you. While in javascript it allows me without a warning. That's why it seems as if things work differently in javascript, if it allows me to (not) use await freely. (I don't remember if c# was just a warning or an error).
- neonsunset 2y agoNo. It is best to verify assumptions first before presenting them as facts. It is irritating to see this everywhere as the quality of discussion keeps going down. In order to get the result of a promise in JS you have to await it (or chain it with 'then', much like .NET's 'ContinueWith' although it is usually discouraged). Consumption of tasks in .NET follows a similar pattern. Async implementations in .NET and Python have also vastly different performance implications (on top of pre-existing unacceptable performance of Python in general). The analyzer gives you a warning for unawaited tasks where not awaiting task indicates a likely user error. Which is why if you want to fire and forget a task, you write it like '_ = http.PostAsync(url, content);` - it also indicates the intention clearly.
- nextcaller 2y agoOh sorry for irritating you, I will return to my pod.
- exabrial 2y ago> The language that I think actually go this right is modern Java. Project Loom in Java has coroutines and all the bells and whistles under the hood, but what it exposes to the developer is good old threads. There are virtual threads, which are mounted on carrier OS threads, and these virtual threads can travel from thread to thread. If you end up issuing a blocking call on a virtual thread, it yields to the scheduler. I completely agree! They studied a lot of "bad" implementations and moved slow to get it right
- huem0n 2y ago> The closest equivalent would be a stupid function that calls a very long running sleep I disagree. The equivalent would be a thread that's never joined, like a thread with an infinite loop
- mst 2y ago> Stackless did not have a bright future because the stackless nature meant that you could not have interleaving Python -> C -> Python calls and suspend with them on the stack. perl's Coro.pm (https://p3rl.org/Coro https://p3rl.org/Coro) includes a C-level coroutine implementation so if you call back into perl code from C code, it marks that underlying coroutine as occupied until it returns back through the C code into something that's perl all the way down. (I got rapidly annoyed with it because I kept managing to segfault the damn thing, but I am the sort of programmer who seems to manage to break everything, and a bunch of other people whose abilities I respect have built substantial systems using it without suffering from the same; please either assume for the sake of argument that I was holding it wrong, or at least consider that the concept is extremely neat even if a tad on the tricky side to make fully robust)
- vanderZwan 2y agoI'm a little annoyed with the repeated claim in the article that "threads" do not exist in JavaScript when the MDN page on web workers[0] starts with: > Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application. Sure, it's not like a unix thread, it's message-passing based instead. Because event-based concurrency came first, and so the natural way to add threads to it was to make them message-passing based. Given that the author expresses their love of the actor model makes it extra surprising they don't mention it, since it seems to be pretty much what they want semantically speaking. If you want to avoid promises you can also have lots of fun with workers and message channels[1] to implement your own concurrency approaches. What I'm even more annoyed by is that everyone always acts like Nathaniel J. Smith invented structured concurrency. There's an entire field of overlooked concurrency paradigms in the family of dataflow programming languages[2], which goes back all the way to the eighties. My personal recent-ish favorite among them being Céu[3][4]. Funny enough it's also yet another language with a completely different take on what "async" and "await" mean. Céu an imperative language centered where the concurrency used both asynchronous events and sychronous ones. The former represents external events. The latter internal ones. Code flow can be split into synchronous concurrent paths by writing out each path directly, similar to if .. else .. branches. Instead of "branches" they're called "trails", and started with par/or and par/and as keywords. par/or do loop do await 1s; _printf("Hello "); await 1s; _printf("World!\n"); end with await async do emit 10s; end end Trails are visited in lexical order. If an "await" keyword is encountered the trail is suspended until the event it waits for fires. This is the benefit of synchronous, single-threaded concurrency: have the benefit of allowing the order in which events fire deterministic and expressible in lexical order. As mentioned, synchronous events represent external inputs. From the perspective of a single module of single-threaded code. they're queued up and get resolved one at a time after the current series of synchronous events finishes and all activated trails in the module are suspended (i.e. awaiting another external event to wake one or more of them in lexical order). The "async" keyword exists solely to simulate such an asynchronous external event inside the code. In the above example, the "emit 10s" simulates passing 10 seconds of time (and could also have been written as a simple "await 5s", but then it wouldn't have demonstrated the async keyword). So in the above example, the first trail is entered, starting a loop. Then at each "await 1s" it suspends until one second has passed, after which it resumes from where it awaited. The second trail is entered after the first trails suspends the first time, then suspends for ten seconds. After that the second trail finishes. Because we split into concurrent trails using "par/or", all trails are aborted whenever any trail finishes. This means that we should have printed "Hello World!" five times. However, if the two trails had been written in reverse order: par/or do await async do emit 10s; end with loop do await 1s; _printf("Hello "); await 1s; _printf("World!\n"); end end ... the first trail would have finished first, aborting the last "await 1s" of the second trail, resulting in one half finished "Hello " without a newline. The neat thing is that all of the synchronous concurrency can be compiled down to a very minimal finite state machine with microscopic memory overhead, making it usable in embedded contexts. (Tangent: the above is the old syntax btw, it is currently being rewritten into a version called Dynamic Céu with more features, and a different syntax whose terminology is also more in line with what the mainstream consensus is for various concurrency terms[5][6]). [0] https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers... [1] https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel https://developer.mozilla.org/en-US/docs/Web/API/MessageChan... [2] https://en.wikipedia.org/wiki/Dataflow_programming https://en.wikipedia.org/wiki/Dataflow_programming [3] https://github.com/ceu-lang/ceu https://github.com/ceu-lang/ceu [4] https://ceu-lang.github.io/ceu/out/manual/v0.30/ https://ceu-lang.github.io/ceu/out/manual/v0.30/ [5] https://github.com/fsantanna/dceu?tab=readme-ov-file#the-programming-language-ceu-v05 https://github.com/fsantanna/dceu?tab=readme-ov-file#the-pro... [6] https://github.com/fsantanna/dceu/blob/main/doc/manual-out.md https://github.com/fsantanna/dceu/blob/main/doc/manual-out.m...