I think it's a great idea to not have to have two libraries - so its a "tick" from me for any idea that permits it.
The thing that bothers me in general about asynchronous code is how you test it so that you know with some confidence that if it passes the tests today you have replicated all the scenarios/orderings that might happen in production.
You have this same problem with threads of course and I've always found multithreaded programs to be much harder to write and debug....such that I personally use threading only when I feel I have to.
The actual problem with it is that caution is communicating it to developers. I recently had to work on a python system where the developers were obviously doing Javascript half the time. So ... hooray.... they put out a huge changeset to make the thing async....and threaded. Oddly enough none of them had ever heard of the GIL and I got the feeling of being seen as an irritating old bastard as I explained it to their blank stares. Didn't matter. Threading is good. Then I pointed out that their tests were now always passing no matter if they broke the code. Blank stares. They didn't realise that mangum forced all background tasks and async things to finish at the end of an HTTP request so their efforts to shift processing to speed up the response were for nothing.
Knowing things doesn't always matter if you cannot get other people to see them.
We plan to have in Zig a testing `Io` implementation that will potentially use fuzzing to stress test your code under a concurrent execution model.
That said, I think a key insight is that we expect most of the library code out there to not do any calls to `io.async` or `io.asyncConcurrent`. Most database libraries for example don't need any of this and will still contain simple synchronous code. But then that code will be able to be used by application developers to express asynchrony at a higher level:
io.async(writeToDb)
io.async(doOtherThing)
Which makes things way less error prone and simpler to understand than having async/await sprinkled all over the place.
That resonates. Testing asynchronous and multithreaded code for all possible interleavings is notoriously difficult. Even with advanced fuzzers or concurrency testing frameworks, you rarely gain full confidence without painful production learnings.
In distributed systems, it gets worse. For example, when designing webhook delivery infrastructure, you’re not just dealing with async code within your service but also network retries, timeouts, and partial failures across systems. We ran into this when building reliable webhook pipelines; ensuring retries, deduplication, and idempotency under high concurrency became a full engineering problem in itself.
That’s why many teams now offload this to specialized services like Vartiq.com, which handles guaranteed webhook delivery with automatic retries and observability out of the box. It doesn’t eliminate the async testing problem within your own code, but it reduces the blast radius by abstracting away a chunk of operational concurrency complexity.
Totally agree though – async, threading, and distributed concurrency all amplify each other’s risks. Communication and system design caution matter more than any syntax or library choice.
Defining async is hard. And I'm writing this as one of the many people who designed async in JavaScript.
I don't quite agree with the definition in this post: just because it's async doesn't mean that it's correct. You can get all sorts of user-land race conditions with async code, whether it uses `async`/`await` (in languages that need/support it) or not.
My latest formulation (and I think that it still needs work) is that async means that the code is explicitly structured for concurrency.
The important thing to me is to distinguish between the abstract concept of asynchronism and how it can be implemented, and by the latter I mean both at the abstract level of a programming language and by technical coordination means in a machine. For the abstract concept at the highest level, well, it is just the dual of synchronism: two (or more) parties that somehow need to work together (i.e., one has a dependency on the other in a way that certain things need to happen before another one can continue) are not synchronized, meaning that it is not known or not defined when the things that need to happen after something else will be done. Seen that way, this definition is not hard. The hard thing can be the abstract means designed in a language: the amount of cognitive effort it takes in order to comprehended them and/or use them (in an fault-free way).
Largely agreed: async means that the code is structured in such a manner that the only way to be certain that a task is complete is to perform some rendez-vous. By extension, it covers mechanisms to make this happen in your code.
I am not deep into the matter but I would have given the answer: Async code is making code that would have been blocking non-blocking in a manner other stuff can still happen while it is being completed.
Since I work a lot in embedded loops where long running blocking snippets could literally break your I/O, lead to visible/audible dropouts etc. this would be the obvious answer.
But that's the thing: async, by itself, doesn't guarantee that anything is non-blocking. For your fiber (or any other kind of user-land abstraction) to be non-blocking, you MUST ensure that it doesn't perform any blocking call.
All async does is give you (some of) the tools to make code non-blocking.
Yeah, sure I mean in embedded-land any async snippet could perform any number of things, like firing a delay command that puts the whole processor to sleep.
This could potentially be avoided by clever enough compilers or runtimes, but I am not sure whether that would really be benefitial.
I am a fan of making things explicit, so the closer peoples idea of what aync is and what it isn't matches reality the better. Alternatively we should get the definition of what async should be clear first and then make the adjustment to the abstractions so they give us the guarantees people would natuarally assume come with that.
Yeah, I'm insisting because I recently reviewed a PR with async code calling blocking code, which made the entire exercise pointless. And that was from an experienced dev.
There used to be a few compilers that used static analysis to predict the cost of a call (where the cost of I/O was effectively considered infinite) and in which you could enforce that a branch only had a budget of N. Modern architectures tend to mess up with any finite value of N, but you could fairly easily adapt such techniques to detect unbounded values.
If you look on the bright side, it looks like free-threading is approaching, and OCaml has demonstrated how, by removing the GIL and adding exactly one primitive, you can turn a powerful enough language into a concurrency/parallelism powerhouse with minimal user-visible changes!
"Asynchrony" is a very bad word for this and we already have a very well-defined mathematical one: commutativity. Some operations are commutative (order does not matter: addition, multiplication, etc.), while others are non-commutative (order does matter: subtraction, division, etc.).
Usually, ordering of operations in code is indicated by the line number (first line happens before the second line, and so on), but I understand that this might fly out the window in async code. So, my gut tells me this would be better achieved with the (shudder) `.then(...)` paradigm. It sucks, but better the devil you know than the devil you don't.
As written, `asyncConcurrent(...)` is confusing as shit, and unless you memorize this blog post, you'll have no idea what this code means. I get that Zig (like Rust, which I really like fwiw) is trying all kinds of new hipster things, but half the time they just end up being unintuitive and confusing. Either implement (async-based) commutativity/operation ordering somehow (like Rust's lifetimes maybe?) or just use what people are already used to.
> As written, `asyncConcurrent(...)` is confusing as shit, and unless you memorize this blog post, you'll have no idea what this code means. I get that Zig (like Rust, which I really like fwiw) is trying all kinds of new hipster things, but half the time they just end up being unintuitive and confusing. Either implement (async-based) commutativity/operation ordering somehow (like Rust's lifetimes maybe?) or just use what people are already used to.
I can't agree. It is confusing, because you need to remember the blog post, it wouldn't be confusing in the slightest if you internalized the core idea. The question remains: is it worth it to internalize the idea? I don't know, but what I do know is some people will internalize it and try to do a lot of shit with this in mind, and after a while we will be able to see where this path leads to. At that point we will be able to decide if it is a good idea or not.
> "Asynchrony" is a very bad word for this and we already have a very well-defined mathematical one: commutativity.
It is risky to use "commutativity" for this. Zig has operators, and some of them are commutative. And it will be confusing. Like if I wrote `f() + g(). Addition is commutative, then Zig is free to choose to run f() and g() in parallel. The order of execution and commutativity are different things. Probably one could tie them into one thing with commutative/non-commutative operators, but I'm not sure it is a good idea, and I'm sure that this is the completely different issue to experimenting with asynchrony.
I'm not sure they are that different, you could just as well store function calls in some constant each on its line then addition the result on a third. This is only syntax, not conceptual difference here. And on practical level, the difference is that the operator can be directly matched with some machine instruction, with operands being native data type such as integer.
Still, you might then prefer a word as permutability, or swappability.
Strictly speaking commutativity is defined over (binary) operations - so if one were to say that two async statements (e.g. connect/accept) are commutative, I would have to ask, "under what operation?"
Currently my best answer for this is the bind (>>=) operator (including, incidentally, one of its instances, `.then(...)`), but this is just fuzzy intuition if anything at all.
It's a good intuition. This has been studied extensively, the composition rule that is lax enough to permit arbitrary effects but strict enough to guarantee this class of outcomes is (>>=). We can keep trying to cheat this as long as we want, but it's bind.
Commutative operations (all of them I think?) are trivially generalized to n-ary operations (in fact, we do this via ∑ and ∏, in the case of addition and multiplication, respectively). You're right that the question of what "operation" we're dealing with here is a bit hazy; but I'd wager that it's probably in the family of the increment operation (N++ === N + 1 = 1 + N) since we're constantly evaluating the next line of code, like the head of a Turing machine.
Edit: maybe it's actually implication? Since the previous line(s) logically imply the next. L_0 → L_1 → L_2 → L_n? Though this is non-commutative. Not sure, it's been a few years since my last metalogic class :P
Implication sounds right. With no further analysis, running each line in order is correct (for whatever "order" is defined by a language, let's assume imperative).
A compiler could recognise that e.g. L_2 doesn't depend on L_1, and would be free to reorder them. And compilers do recognise this in terms of data dependence of operations.
Generalizing an associative binary op to an n-ary op just requires an identity element Id (which isn't always obvious, e.g. Id_AND=true but Id_OR=false).
> Generalizing an associative binary op to an n-ary op just requires an identity element Id (which isn't always obvious, e.g. Id_AND=true but Id_OR=false).
Only for n = 0, I think. Otherwise, generalizing associative binary f_2 to f_n for all positive integers n is easily done inductively by f_1(x) = x and f_{n + 1}(x_1, ..., x_n, x_{n + 1}) = f_2(f_n(x_1, ..., x_n), x_{n + 1}), with no need to refer to an identity. (In fact, the definition makes sense even if f_2 isn't associative, but is probably less useful because of the arbitrary choice to "bracket to the left.")
The "operator" in this case would be the CPU executing 2 or N procedures (or functions).
Commutivity is a very light weight pattern, and so is correctly applicable to many things, and at any level of operation, as long as the context is clear.
`.then()` is ugly, `await` is pretty, but wouldn't the critical part to guarantee commutivity less than guaranteed order (in js) be the `Promise.all([])` part?
Asynchrony also allows for partial ordering. Two operations may still need to be retired in a particular order without having to execute in that order.
Subtraction for instance is not commutative. But you could calculate the balance and the deduction as two separate queries and then apply the results in the appropriate order.
> "Asynchrony" is a very bad word for this and we already have a very well-defined mathematical one: commutativity.
I don't think it's sufficient to say that just because another term defines this concept means it's a better or worse word. "commutativity" feels, sounds, and reads like a mess imo. Asynchrony is way easier on the palette
commutativity is also not correct, because 1) it means way more things than just temporal ordering and 2) there are cooky temporal ordering schemes you can come up with (interleaving multiple async/awaits in weird time-dependent ways) which aren't really describable in the simple mathematical notion of commutativity.
> So, my gut tells me this would be better achieved with the (shudder) `.then(...)` paradigm. It sucks, but better the devil you know than the devil you don't.
The whole idea behind `await` is to make the old intuition work without the ugliness of `.then()`. `f(); await g(); h()` has exactly the expected execution ordering.
In JS, we designed `await` specifically to hide `.then()`, just as we had designed `.then()` because callbacks made tracking control flow (in particular errors) too complex.
Well, one of the ways we "sold" async/await it to Google was by showing how we could improve Promise-based tests.
I recall that one of our test suites was tens of thousands of lines of code using `then()`. The code was complicated enough that these lines were by and large considered write-only, partly because async loops were really annoying to write, partly because error-handling was non-trivial.
I rewrote that test suite using `Task.spawn` (our prototype for async/await). I don't have the exact numbers in mind, but this decreased the number of LoC by a factor of 2-3 and suddenly people could see the familiar uses of loops and `try`/`catch`.
Commutativity is a much weaker claim because one is totally before or after the other. e.g. AB may commute with C so ABC=CAB but it is not necessarily the case that this equals ACB. With asynchrony you are guaranteed ABC=ACB=CAB. (There may be an exisiting mathematical term for this but I don't know it)
I'm not talking about a universe where all elements commute, I'm talking about a situation in which A, B, and C do not necessarily commute but (AB) and C do. For a rigorous definition: given X and Y from some semigroup G, say X and Y are asynchronous if for any finite decompositions X=Z_{a_1}Z_{a_2}...Z_{a_n} and Y=Z_{b_1}Z_{b_2}...Z_{b_m} (with Z's in G) then for any permutation c_1,...,c_{n+m} of a_1,...,a_n,b_1,...,b_m that preserves the ordering of a's and the ordering of the b's has XY=Z_{c_1}Z_{c_2}...Z_{c_{n+m}}. I make the following claim: if G is commutative then all elements are asynchronous, but for a noncommutative G there can exist elements X and Y that commute (i.e. XY=YX) but X and Y are not asynchronous.
To give a concrete example, matrix multiplication is not commutative in general (AB ≠ BA), but e.g. multiplication with the identity matrix is (AI = IA). So AIB = ABI ≠ BAI.
Or applied to the programming example, the statements:
So "cooperative multitasking is not preemptive multitasking".
The typical use of the word "asynchronous" means that the _language is single-threaded_ with cooperative multitasking (yield points) and event based, and external computations may run concurrently, instead of blocking, and will report result(s) as events.
There is no point in having asynchrony in a multithreaded or concurrent execution model, you can use blocking I/O and still have progress in the program while that one execution thread is blocked.
Then you don't need the yield points to be explicit.
While this is indeed the most common use, I'll bring as counter-examples Rust (or C#, or F#, or OCaml 5+) that supports both OS threads and async. OS threads are good for CPU-bound tasks, async for IO-bound tasks.
The main benefit of having async (or Go-style M:N scheduling) is that you can afford to launch as many tasks/fibers/goroutines/... as you want, as long as you have RAM. If you're using OS threads, you need to pool them responsively to avoid choking your CPU with context-switches, running out of OS threads, running out of RAM, etc. – hardly impossible, but if you're doing more than just I/O, you can run into interesting deadlocks.
> The main benefit of having async (or Go-style M:N scheduling) is that you can afford to launch as many tasks/fibers/goroutines/... as you want
Some have argued that the real solution to this problem is to "just" fix OS threads. Rumor has it Google has done exactly this, but keeps it close to their chest:
Somewhat related and also by Google is WebAssembly Promise Integration, which converts blocking code into non-blocking code without requiring language support:
I kind of think the author simply pulled the concept of yielding execution out of the definition of concurrency and into this new "asynchrony" term. Then they argued that the term is needed because without it the entire concept of concurrency is broken.
Indeed so, but I would argue that concurrency makes little sense without the ability to yield and is therefore intrinsic to it. Its a very important concept but breaking it out into a new term adds confusion, instead of reducing it.
I'd count pure one-to-one parallelism as a form of concurrency that doesn't involve any yielding. But otherwise, I agree that all forms of non-parallel concurrency have to be yielding execution at some cadence, even if it's at the instruction level. (E.g., in CUDA, diverging threads in a warp will interleave execution of their instructions, in case one branch tries blocking on the other.)
Well I'm having a hell of a time understanding what this article is trying to say. On a 3rd and 4th pass I think perhaps they mean task (in)dependency tracking is a fundamental concept. Independent tasks have "asynchrony." (Can we just say dependency and independency?)
But even with that definition, it seems like the idea of promises, task tracking, etc is well tread territory.
Then they conclude with how fire and forget tasks solve coloring but isn't that just the sync-over-async anti-pattern? I wouldn't be excited that my UI work stops to run something when there are no more green threads but they seem excited by it.
Anyway, I guess I got too distracted by the high concept "this is a fundamental change in thinking" fluff of the article.
Asynchrony, in this context, is an abstraction which separates the preparation and submission of a request from the collection of the result.
The abstraction makes it possible to submit multiple requests and only then begin to inquire about their results.
The abstraction allows for, but does not require, a concurrent implementation.
However, the intent behind the abstraction is that there be concurrency. The motivation is to obtain certain benefits which will not be realized without concurrency.
Some asynchronous abstractions cannot be implemented without some concurrency. Suppose the manner by which the requestor is informed about the completion of a request is not a blocking request on a completion queue, but a callback.
Now, yes, a callback can be issued in the context of the requesting thread, so everything is single-threaded. But if the requesting thread holds a non-recursive mutex, that ruse will reveal itself by causing a deadlock.
In other words, we can have an asynchronous request abstraction that positively will not work single threaded;
1 caller locks a mutex
2 caller submits request
3 caller unlocks mutex
4 completion callback occurs
If step 2 generates a callback in the same thread, then step 3 is never reached.
The implementation must use some minimal concurrency so that it has a thread waiting for 3 while allowing the requestor to reach that step.
Completely agree. The server/client example in the post was just one example of a program not being able to make progress, you’ve just gave another which cannot be solved the same way, and I would bet there are many more that they will be discovering over time. IMO when async is used, concurrency needs to be ensured.
They're just different from what Lamport originally proposed. Asynchrony as given is roughly equivalent to Lamport's characterization of distributed systems as partially ordered, where some pairs of events can't be said to have occurred before or after one another.
One issue with the definition for concurrency given in the article would seem to be that no concurrent systems can deadlock, since as defined all concurrent systems can progress tasks. Lamport uses the word concurrency for something else: "Two events are concurrent if neither can causally affect the other."
Probably the notion of (a)causality is what the author was alluding to in the "Two files" example: saving two files where order does not matter. If the code had instead been "save file A; read contents of file A;" then, similarly to the client connect/server accept example, the "save" statement and the "read" statement would not be concurrent under Lamport's terminology, as the "save" causally affects the "read."
It's just that the causal relationship between two tasks is a different concept than how those tasks are composed together in a software model, which is a different concept from how those tasks are physically orchestrated on bare metal, and also different from the ordering of events..
The definition of asynchrony is bad. It's possible for asynchronous requests to guarantee ordering, such that if a thread makes two requests A and B in that order, asynchronously, they will happen in that order.
Asynchrony means that the requesting agent is not blocked while submitting a request in order to wait for the result of that request.
Asynchronous abstractions may provide a synchronous way wait for the asynchronously submitted result.
> The definition of asynchrony is bad. It's possible for asynchronous requests to guarantee ordering, such that if a thread makes two requests A and B in that order, asynchronously, they will happen in that order.
It's true that it's possible - two async tasks can be bound together in sequence, just as with `Promise.then()` et al.
... but it's not necessarily the case, hence the partial order, and the "possibility for tasks to run out of order".
For example - `a.then(b)` might bind tasks `a` and `b` together asynchronously, such that `a` takes place, and then `b` takes place - but after `a` has taken place, and before `b` has taken place, there may or may not be other asynchronous tasks interleaved between `a` and `b`.
The ordering between `a`, `b`, and these interleaved events is not defined at all, and thus we have a partial order, in which we can bind `a` and `b` together in sequence, but have no idea how these two events are ordered in relation to all the other asynchronous tasks being managed by the runtime.
I mean that it's possible in the sense of being designed in as a guarantee; that the async operations issued against some API object will be performed in the order in which they are submitted, like a FIFO queue.
I don't mean "promise.then", whereby the issuance of the next request is gated on the completion of the first.
An example might be async writes to a file. If we write "abc" at the start of the file in one request and "123" starting at the second byte in the second requests, there can be a guarantee that the result will be "a123", and not "abc2", without gating on the first request completing before starting the other.
async doesn't mean out of order; it means the request initiator doesn't synchronize on the completion as a single operation.
Concurrency is the property of a program to be divided into partially ordered or completely unordered units of execution. It does not describe how you actually end up executing the program in the end, such as if you wish to exploit these properties for parallel execution or task switching. Or maybe you're running on a single thread and not doing any task switching or parallelism.
For more I'd look up Rob Pike's discussions for Go concurrency.
Is the hang up on "at a time"? What if that were changed to something like "in a given amount of time"?
For single threaded programs, whether it is JS's event loop, or Racket's cooperative threads, or something similar, if Δt is small enough then only one task will be seen to progress.
careful: in many programming contexts parallelism and concurrency are exclusive concepts, and sometimes under the umbrella of async, which is a term that applies to a different domain.
in other contexts these words don't describe disjoint sets of things so it's important to clearly define your terms when talking about software.
What people mean by "concurrency is not parallelism" is that they are different problems. The concurrency problem is defining an application such that it has parts that are not causally linked with some other parts of the program. The parallelism problem is the logistics of actually running multiple parts of your program at the same time. If I write a well-formed concurrent system, I shouldn't have to know or care if two specific parts of my system are actually being executed in parallel.
In ecosystems with good distributed system stories, what this looks like in practice is that concurrency is your (the application developers') problem, and parallelism is the scheduler designer's problem.
yes, some people swap the meaning of concurrency and asynchrony. But, almost all implementations of async use main event loops, global interpreter lock, co-routines etc. and thus at the end of the day only do one thing at a time.
Therefore I think this definition makes the most sense in practical terms. Defining concurrency as the superset is a useful construct because you have to deal with the same issues in both cases. And differentiating asynchrony and parallelism makes sense because it changes the trade-off of latency and energy consumption (if the bandwidth is fixed).
> Asynchrony: the possibility for tasks to run out of order and still be correct.
Asynchrony is when things don't happen at the same time or in the same phase, i.e. is the opposite of Synchronous. It can describe a lack of coordination or concurrence in time, often with one event or process occurring independently of another.
The correctness statement is not helpful. When things happy asynchronously, you do not have guarantees about order, which may be relevant to "correctness of your program".
Undefined behavior from asynchronous computing is not worth study or investment, except to avoid it.
Virtually all of the effort for the last few decades (from super-scalar processors through map/reduce algorithms and Nvidia fabrics) involves enabling non-SSE operations that are correct.
So yes, as an abstract term outside the context of computing today, asynchrony does not guarantee correctness - that's the difficulty. But the only asynchronous computing we care about offers correctness guarantees of some sort (often a new type, e.g., "eventually consistent").
The phrase "multiple tasks at a time" is ill-defined, according to Lamport, because whose clock are you trusting.
For lamport concurrent does not mean what it means to us colloquially or informally (like, "meanwhile"). Concurrency in Lamport's formal definition is only about order. If one task is dependent or is affected by another, then the first is ordered after the second one. Otherwise, they are deemed to be "concurrent", even if one happens years later or before.
Not the OP, but in formal definitions like Communicating Sequential Processes, concurrency means the possibility for tasks to run out of order and still be correct, as long as other synchronisation events happen
But if I run "ls" on a machine, and another user runs "ls" on the same machine, wouldn't you consider them independent, even though the OS uses all kinds of locks and what not under the hood?
Doesn't multiple tasks at the same time make it simultaneous?
I think there needs to be a stricter definition here.
Concurrency is the ability of a system to chop a task into many tiny tasks. A side effect of this is that if the system chops all tasks into tiny tasks and runs them all in a sort of shuffled way it looks like parallelism.
This is why I've completely stopped using the term, literally everyone I talk to seems to have a different understanding. It no longer serves any purpose for communication.
Keep in mind that you can’t really express half the concepts in lamport’s papers in most languages. You don’t really talk about total and partial clock ordering when starting a thread. You only really do it in TLA+ when designing a protocol.
That being said, I agree we don’t need a new term to express “Zig has a function in the async API that throws a compilation error when you run in a non-concurrent execution. Zig let’s you say that.” It’s fine to so that without proposing new theory.
The author is aware that definitions exist for the terms he uses in his blog post. He is proposing revised definitions. As long as he is precise with his new definitions, this is fine. It is left to the reader to decide whether to adopt them.
He’s repurposing asynchrony that’s different from the way most literature and many developers use it, and that shift is doing rhetorical work to justify a particular Zig API split.
The new Zig I/O idea seems like a pretty ingenious idea, if you write mostly applications and don't need stackless coroutines. I suspect that writing libraries using this style will be quite error-prone, because library authors will not know whether the provided I/O is single or multi-threaded, whether it uses evented I/O or not... Writing concurrent/async/parallel/whatever code is difficult enough on its own even if you have perfect knowledge of the I/O stack that you're using. Here the library author will be at the mercy of the IO implementation provided from the outside. And since it looks like the IO interface will be a proper kitchen sink, essentially an implementation of a "small OS", it might be very hard to test all the potential interactions and combinations of behavior. I'm not sure if a few async primitives offered by the interface will be enough in practice to deal with all the funny edge cases that you can encounter in practice. To support a wide range of IO implementations, I think that the code would have to be quite defensive and essentially assume the most parallel/concurrent version of IO to be used.
It will IMO also be quite difficult to combine stackless coroutines with this approach, especially if you'd want to avoid needless spawning of the coroutines, because the offered primitives don't seem to allow expressing explicit polling of the coroutines (and even if they did, most people probably wouldn't bother to write code like that, as it would essentially boil down to the code looking like "normal" async/await code, not like Go with implicit yield points). Combined with the dynamic dispatch, it seems like Zig is going a bit higher-level with its language design. Might be a good fit in the end.
It's quite courageous calling this approach "without any compromise" when it has not been tried in the wild yet - you can claim this maybe after 1-2 years of usage in a wider ecosystem. Time will tell :)
> It will IMO also be quite difficult to combine stackless coroutines with this approach
Maybe there will be unforeseen problems, but they have promised to provide stackless coroutines; since it's needed for the WASM target, which they're committed to supporting.
> Combined with the dynamic dispatch
Dynamic dispatch will only be used if your program employs more than one IO implementation. For the common case where you're only using a single implementation for your IO, dynamic dispatch will be replaced with direct calls.
> It's quite courageous calling this approach "without any compromise" when it has not been tried in the wild yet.
You're right. Although it seems quite close to what "Jai" is purportedly having success with (granted with an implicit IO context, rather than an explicitly passed one). But it's arguable if you can count that as being in the wild either...
> I think that the code would have to be quite defensive and essentially assume the most parallel/concurrent version of IO to be used.
Exactly, but why would anyone think differently when the goal is to support both synchronous and async execution?
However, if asynchrony is done well at the lower levels of IO event handler, it should be simple to implemcent by following these principles everywhere — the "worst" that could happen is that your code runs sequentially (thus slower), but not run into races or deadlocks.
I think I'm missing something here, but the most interesting piece here is how would stackless coroutines work in Zig?
Since any function can be turned into a coroutine, is the red/blue problem being moved into the compiler? If I call:
io.async(saveFileA, .{io});
Is that a function call? Or is that some "struct" that gets allocated on the stack and passed into an event loop?
Furthermore, I guess if you are dealing with pure zig, then its fine, but if you use any FFI, you can potentially end up issuing a blocking syscall anyways.
I hope this is not a bad answer as I tried to understand what stackless coroutines even are for the past week.
1. Zig plans to annotate the maximum possible stack size of a function call https://github.com/ziglang/zig/issues/23367 . As people say, this would give the compiler enough information to implemented stackless coroutines. I do not understand well enough why that’s the case.
2. Allegedly, this is only possible because zig uses a single compilation unit. You are very rarely dealing with modules that are compiled independently. If a function in zig is not called, it’s not compiled. I can see how this helps with point 1.
3. Across FFI boundaries this is a problem in every language. In theory you can always do dumb things after calling into a shared library. A random C lib can always spawn threads and do things the caller isn’t expecting. You need unsafe blocks in rust for the same reason.
4. In theory, zig controls the C std library when compiling C code. In some cases, if there’s only one Io implementation used for example, zig could replace functions in the c std library to use that io vtable instead.
Regardless, I kinda wish kristoff/andrew went over what stackless coroutines are (for dummies) in an article at some point. I am unsure people are talking about the same thing when mentioning that term. I am happy to wait for that article until zig tries to implement that using the new async model.
The author does not seem to have made any non-trivial projects with asynchronicity.
All the pitfalls of concurrency are there - in particular when executing non-idempotent functions multiple times before previous executions finish, then you need mutexes!
> All the pitfalls of concurrency are there [in async APIs]
This is one of those "in practice, theory and practice are different" situations.
There is nothing in the async world that looks like a parallel race condition. Code runs to completion until it deterministically yields, 100% of the time, even if the location of those yields may be difficult to puzzle out.
And so anyone who's ever had to debug and reason about a parallel race condition is basically laughing at that statement. It's just not the same.
> Code runs to completion until it deterministically yields
No, because async can be (quote often is) used to perform I/O, whose time to completion does not need to be deterministic or predictable. Selecting on multiple tasks and proceeding with the one that completes first is an entirely ordinary feature of async programming. And even if you don't need to suffer the additional nondeterminism of your OS's thread scheduler, there's nothing about async that says you can't use threads as part of its implementation.
And I repeat, if you think effects from unpredictable I/O completion order constitute equivalent debugging thought landscapes to hardware-parallel races, I can only laugh.
Yes yes, in theory they're the same. That's the joke.
You're not reporting yourself, though, since you didn't make it clear at all in your initial comment you were talking about hardware. The previous comment you made only mentioned "parallel data races" in a conversation about software ecosystems, where both of those terms are regularly used to describe things that occur. You're laughing about dunking on people who you've run up to in the middle of a football field; no one stopped you from scoring because you didn't tell them that you're apparently playing an entirely different game on your own.
You don't need mutex in async code, since there is no parallel execution whatsoever. In fact languages that use async programming as a first class citizen (JavaScript) don't even have a construct to do them.
If you need to synchronize stuff in the program you can use normal plain variables, since it's guaranteed that your task will be never interrupted till you give control back to the scheduler by performing an await operation.
In a way, async code can be used to implement mutex (or something similar) themself: it's a technique that I use often in JavaScript, to implement stuff that works like a mutex or a semaphores with just promises to syncronize stuff (e.g. you want to be sure that a function that itself does async operations inside is not interrupted, it's possible to do so with promises and normal JS variables).
> You don't need mutex in async code, since there is no parallel execution whatsoever. In fact languages that use async programming as a first class citizen (JavaScript) don't even have a construct to do them.
This isn't even remotely true; plenty of languages have both async and concurrency, probably more than ones that don't. C# was the language that originated async/await, not JavaScript, and it certainly has concurrency, as do Swift, Python. Rust, and many more. You're conflating two independent proprieties of JavaScript as language and incorrectly inferring a link between them that doesn't actually exist.
See this is what the OP is getting at, this is only true for async implementations that don't have any parallelism. That doesn't have to be the case, there's no reason that your javascript runtime couldn't take
await foo()
await bar()
and execute them in two threads transparently for you. It just happens, like the Python GIL, that it doesn't. Your JS implementation actually already has mutexes because web workers with shared memory bring true parallelization along with the challenges that come with.
In the case of javascript, it's only allowed to do that when you can't detect it, situations where foo doesn't affect the output of bar. So as far as pitfalls are concerned it does one thing at a time. The rest is a hidden implementation detail of the optimizer.
I can do a lot of things asynchronously. Like, I'm running the dishwasher AND the washing machine for laundry at the same time. I consider those things not occurring at "the same time" as they're independent of one another. If I stood and watched one finish before starting the other, they'd be a kind of synchronous situation.
But, I also "don't care". I think of things being organized concurrently by the fact that I've got an outermost orchestration of asynchronous tasks. There's a kind of governance of independent processes, and my outermost thread is what turns the asynchronous into the concurrent.
Put another way. I don't give a hoot what's going on with your appliances in your house. In a sense they're not synchronized with my schedule, so they're asynchronous, but not so much "concurrent".
So I think of "concurrency" as "organized asynchronous processes".
Does that make sense?
Ah, also neither asynchronous nor concurrent mean they're happening at the same time... That's parallelism, and not the same thing as either one.
I think asynchronous as meaning out-of-sync, implies that there needs to be synchronicity between the two tasks.
In that case, asynchronous just means the state that two or more tasks that should be synchronized in some capacity for the whole behavior to be as desired, is not properly in-sync, it's out-of-sync.
Then I feel there can be many cause of asynchronous behavior, you can be out-of-sync due to concurent execution or due to parallel execution, or due to buggy synchronization, etc.
And because of that, I consider asynchronous programming as the mechanisms that one can leverage to synchronize asynchronous behavior.
But I guess you could also think of asynchronous as doesn't need to be synchronized.
The way I like to think about it is that libraries vary in which environments they support. Writing portable libraries that work in any environment is nice, but often unnecessary. Sometimes you don't care if your code works on Windows, or whether it works without green threads, or (in Rust) whether it works without the standard library.
So I think it's nice when type systems let you declare the environments a function supports. This would catch mistakes where you call a less-portable function in a portable library; you'd get a compile error, indicating that you need to detect that situation and call the function conditionally, with a fallback.
I don't get it - the "problem" with the client/server example in particular (which seems pivotal in the explanation). But I am also unfamiliar with zig, maybe that's a prerequisite. (I am however familiar with async, concurrency, and parallelism)
Oh, I see. The article is saying that async is required. I thought it was saying that parallelism is required. The way it's written makes it seem like there's a problem with the code sample, not that the code sample is correct.
The article later says (about the server/client example)
> Unfortunately this code doesn’t express this requirement [of concurrency], which is why I called it a programming error
I gather that this is a quirk of the way async works in zig, because it would be correct in all the async runtimes I'm familiar with (e.g. python, js, golang).
My existing mental model is that "async" is just a syntactic tool to express concurrent programs. I think I'll have to learn more about how async works in zig.
I think a key distinction is that in many application-level languages, each thing you await exists autonomously and keeps doing things in the background whether you await it or not. In system-level languages like Rust (and presumably Zig) the things you await are generally passive, and only make forward progress if the caller awaits them.
This is an artifact of wanting to write async code in environments where "threads" and "malloc" aren't meaningful concepts.
Rust does have a notion of autonomous existence: tasks.
Right, but Go is an application-level language and doesn't target environments where threads aren't a meaningful concept. It's more an artifact of wanting to target embedded environments than something specific to Rust.
But why is this a novel concept? The idea of starvation is well known and you don't need parallelism for it to effect you already. What does zig actually do to solve this?
Many other languages could already use async/await in a single threaded context with an extremely dumb scheduler that never switches but no one wants that.
I'm trying to understand but I need it spelled out why this is interesting.
The novel concept is to make it explicit where a non-concurrent scheduler is enough (async), and where it is not (async-concurrent). As a benefit, you can call async functions directly from a synchronous context, which is not possible for the usual async/await, therefore avoiding the need to have both sync and async versions of every function.
And with green threads, you can have a call chain from async to sync to async, and still allow the inner async function to yield through to the outer async function. This keeps the benefit of async system calls, even if the wrapping library only uses synchronous functions.
There's a great old book on this if someone wants to check it: Communicating Sequential Processes. From Hoare. Go channels and the concurrent approach was inspired on this.
I also wrote a blog post a while back when I did a talk at work, it's Go focused but still worth the read I think.
One thing that most languages are lacking is expressing lazy return values. -> await f1() + await f2() and to express this concurently requres manually handing of futures.
Why is having it be syntax necessary or beneficial?
One might say "Rust's existing feature set makes this possible already, why dedicate syntax where none is needed?"
(…and I think that's a reasonably pragmatic stance, too. Joins/selects are somewhat infrequent, the impediments that writing out a join puts on the program relatively light… what problem would be solved?
vs. `?`, which sugars a common thing that non-dedicated syntax can represent (a try! macro is sufficient to replace ?) but for which the burden on the coder is much higher, in terms of code readability & writability.)
Blocking async code is not async. In order for something to execute "out of order", you must have an escape mechanism from that task, and that mechanism essentially dictates a form of concurrency. Async must be concurrent, otherwise it stops being async. It becomes synchronous.
From the perspective of the application programmer, readA "block" readB. They aren't concurrent.
join(readA, readB).await
In this example, the two operations are interleaved and the reads happen concurrently. The author makes this distinction and I think it's a useful one, that I imagine most people are familiar with even if there is no name for it.
I think that confuses paradigm with configuration. Say, if one thread waits on another to finish, that doesn't mean the code suddenly becomes "single-threaded", it just means your two threads are in a serialized configuration in that instance. Similarly, when async code becomes serialized, it doesn't cease to be async: the scaffolding to make it concurrent is there, it's just unused in that specific configuration.
For example, C# uses this syntax:
await readA();
await readB();
when you have these two lines, the first I/O operation still yields control to a main executor during `await`, and other web requests can continue executing in the same thread while "readA()" is running. It's inherently concurrent, not in the scope of your two lines, but in the scope of your program.
If you need to do A and then B in that order, but you're doing B and then A. It doesn't matter if you're doing B and then A in a single thread, the operations are out of sync.
So I guess you could define this scenario as asynchronous.
> So wait, is the word they mean by asynchrony actually the word "dependency"?
No, the definition provided for asynchrony is:
>> Asynchrony: the possibility for tasks to run out of order and still be correct.
Which is not dependence, but rather independence. Asynchronous, in their definition, is concurrent with no need for synchronization or coordination between the tasks. The contrasted example which is still concurrent but not asynchronous is the client and server one, where the order matters (start the server after the client, or terminate the server before the client starts, and it won't work correctly).
> Which is not dependence, but rather independence
Alright, well, good enough for me. Dependency tracking implies independency tracking. If that's what this is about I think the term is far more clear.
> where the order matters
I think you misunderstand the example. The article states:
> Like before, *the order doesn’t matter:* the client could begin a connection before the server starts accepting (the OS will buffer the client request in the meantime), or the server could start accepting first and wait for a bit before seeing an incoming connection.
The one thing that must happen is that the server is running while the request is open. The server task must start and remain unfinished while the client task runs if the client task is to finish.
> The contrasted example which is still concurrent but not asynchronous is the client and server one
Quote from the post where the opposite is stated:
> With these definitions in hand, here’s a better description of the two code snippets from before: both scripts express asynchrony, but the second one requires concurrency.
You can start executing Server.accept and Client.connect in whichever order, but both must be running "at the same time" (concurrently, to be precise) after that.
If asynchrony, as I quoted direct from your article, insists that order doesn't matter then the client and server are not asynchronous. If the client were to execute before the server and fail to connect (the server is not running to accept the connection) then your system has failed, the server will run later and be waiting forever on a client who's already died.
The client/server example is not asynchronous by your own definition, though it is concurrent.
What's needed is a fourth term, synchrony. Tasks which are concurrent (can run in an interleaved fashion) but where order between the tasks matters.
> If the client were to execute before the server and fail to connect (the server is not running to accept the connection) then your system has failed, the server will run later and be waiting forever on a client who's already died.
From the article:
> Like before, the order doesn’t matter: the client could begin a connection before the server starts accepting (the OS will buffer the client request in the meantime), or the server could start accepting first and wait for a bit before seeing an incoming connection.
When you create a server socket, you need to call `listen` and after that clients can begin connecting. You don't need to have already called `accept`, as explained in the article.
Not necessarily, but I guess it depends how you define "dependency".
For example, it might be partial ordering is needed only, so B doesn't fully depend on A, but some parts of B must happen after some parts of A.
It also doesn't imply necessarily that B is consuming an output from A.
And so on.
But there is a dependency yes, but it could be that the behavior of the system depends on both of them happening in some partial ordering.
The difference is with asynchronous, the timing doesn't matter, just the partial or full ordering. So B can happen a year after A and it would eventually be correct, or at least within a timeout. Or in other words, it's okay if other things happen in between them.
With synchronous, the timings tend to matter, they must happen one after the other without anything in-between. Or they might even need to happen together.
I think there's not much point trying to define these concepts as there is no consensus about what they mean. Different people have clear ideas about what each concept means but they just don't agree.
It's like integration tests vs unit tests... Most developers think they have a clear idea about what each one means, but based on my experience there is very little consensus about where the line is between unit test vs integration test. Some people will say a unit test requires mocking or stubbing out all dependencies, others will say that this isn't necessary; so long as you mock out I/O calls... Others will say unit tests can make I/O calls but not database calls or calls which interface with an external service... Some people will say that if a test covers the module without mocking out I/O calls then it's not an integration test, it's an end-to-end test.
Anyway it's the same thing with asynchrony vs concurrency vs parallelism.
I think most people will agree that concurrency can potentially be achieved without parallelism and without asynchrony. For many people, asynchrony has the connotation that it's happening in the same process and thread (same CPU core). Some people who work with higher level languages might say that asynchrony is a kind of context switching (as it's switching context in the stack when callbacks at called or promises resolved) but system devs will say that context switching is more granular than that and not constrained to the duration of specific operations, they'll say it's a CPU level concept.
“Permission for concurrency is not an obligation for concurrency. Zig lets you explicitly permit-without-obligation, to support the design of libraries that are polymorphic over a/sync ‘function color’.”
> Concurrency refers to the ability of a system to execute multiple tasks through simultaneous execution or time-sharing (context switching)
Wikipedia had the wrong idea about microkernels for about a decade too, so ... here we are I guess.
It's not a _wrong_ description but it's incomplete...
Consider something like non-strict evaluation, in a language like Haskell. One can be evaluating thunks from an _infinite_ computation, terminate early, and resume something else just due to the evaluation patterns.
That is something that could be simulated via generators with "yield" in other languages, and semantically would be pretty similar.
Also consider continuations in lisp-family languages... or exceptions for error handling.
You have to assume all things could occur simultaneously relative to each other in what "feels like" interrupted control flow to wrangle with it. Concurrency is no different from the outside looking in, and sequencing things.
Is it evaluated in parallel? Who knows... that's a strategy that can be applied to concurrent computation, but it's not required. Nor is "context switching" unless you mean switched control flow.
The article is very good, but if we're going by the "dictionary definition" (something programming environments tend to get only "partially correct" anyway), then I think we're kind of missing the point.
The stuff we call "asynchronous" is usually a subset of asynchronous things in the real world. The stuff we treat as task switching is a single form of concurrency. But we seem to all agree on parallelism!
Non-blocking i/o isn't asynchrony and the author should know better. Non-blocking io is a building block of asynchronous systems -- it is not asynchony itself. Today's asynchronous programming did not exist when non-blocking I/O was implemented in Unix in the 80's.
a core problem is that the term async itself is all sorts of terrible, synchronous usually means "happening at the same time", is not what is happening when you don't use `async`
Asynchrony, parallelism, concurrency, and even deterministic execution (albeit as a degenerate case) are all just species of nondeterminism. Dijkstra and Scholten’s work on the subject is sadly under appreciated. And lest one thing this was ivory tower stuff, before he was a professor Dijkstra was a systems engineer writing operating systems on hilariously bad, by our standards, hardware.
The argument about concurrency != parallelism mentioned in this article as being "not useful" is often quoted and rarely a useful or informative, and it also fails to model actual systems with enough fidelity to even be true in practice.
Example: python allows concurrency but not parallelism. Well not really though, because there are lots of examples of parallelism in python. Numpy both releases the GIL and internally uses open-mp and other strategies to parallelize work. There are a thousand other examples, far too many nuances and examples to cover here, which is my point.
Example: gambit/mit-scheme allows parallelism via parallel execution. Well, kindof, but really it's more like python's multiprocess library pooling where it forks and then marshals the results back.
Besides this, often parallel execution is just a way to manage concurrent calls. Using threads to do http requests is a simple example, while the threads are able to execute in parallel (depending on a lot of details) they don't, they spend almost 100% of their time blocking on some socket.read() call. So is this parallelism or concurrency? It's what it is, it's threads mostly blocking on system calls, parallelism vs concurrency gives literally no insights or information here because it's a pointless distinction in practice.
What about using async calls to execute processes? Is that concurrency or parallelism? It's using concurrency to allow parallel work to be done. Again, it's both but not really and you just need to talk about it directly and not try to simplify it via some broken dichotomy that isn't even a dichotomy.
You really have to get into more details here, concurrency vs parallelism is the wrong way to think about it, doesn't cover the things that are actually important in an implementation, and is generally quoted by people who are trying to avoid details or seem smart in some online debate rather than genuinely problem solving.
The difference is quite useful and informative. In fact, most places don't seem to state it strongly enough: Concurrency is a programming model. Parallelism is an execution model.
Concurrency is writing code with the appearance of multiple linear threads that can be interleaved. Notably, it's about writing code. Any concurrent system could be written as a state machine tracking everything at once. But that's really hard, so we define models that allow single-purpose chunks of linear code to interleave and then allow the language, libraries, and operating system to handle the details. Yes, even the operating system. How do you think multitasking worked before multi-core CPUs? The kernel had a fancy state machine tracking execution of multiple threads that were allowed to interleave. (It still does, really. Adding multiple cores only made it more complicated.)
Parallelism is running code on multiple execution units. That is execution. It doesn't matter how it was written; it matters how it executes. If what you're doing can make use of multiple execution units, it can be parallel.
Code can be concurrent without being parallel (see async/await in javascript). Code can be parallel without being concurrent (see data-parallel array programming). Code can be both, and often is intended to be. That's because they're describing entirely different things. There's no rule stating code must be one or the other.
Async JS code is parallel too. For example, await Promise.all(...) will wait on multiple functions at once. The JS event loop is only going to interpret one statement at a time, but in the meantime, other parts of the computer (file handles, TCP/IP stack, maybe even GPU/CPU depending on the JS lib) are actually doing things fully in parallel. A more useful distinction would be, the JS interpreter is single-threaded while C code can be multithreaded.
I can't think of anything in practice that's concurrent but not parallel. Not even single-core CPU running 2 threads, since again they can be using other resources like disk in parallel, or even separate parts of the CPU itself via pipelining.
> A more useful distinction would be, the JS interpreter is single-threaded while C code can be multithreaded.
...this seems like a long way round to say "JS code is not parallel while C code can be parallel".
Or to put it another way, it seems fairly obvious to me that parallelism is a concept applied to one's own code, not all the code in the computer's universe. Other parts of the computer doing other things has nothing to do with the point, or "parallelism" would be a completely redundant concept in this age where nearly every CPU has multiple cores.
When you define some concepts, those definitions and concepts should help you better understand and simplify the descriptions of things. That's the point of definitions and terminology. You are not achieving this goal, quite the opposite in fact, your description is confusing and would never actually be useful in understanding, debugging, or writing software.
Stated another way: if we just didn't talk about concurrent vs parallel we would have exactly the same level of understanding of the actual details of what code is doing, and we would have exactly the same level of understanding about the theory of what is going on. It's trying to impose two categories that just don't cleanly line up with any real system, and it's trying to create definitions that just aren't natural in any real system.
Parallel vs concurrent is a bad and useless thing to talk about. It's a waste of time. It's much more useful to talk about what operations in a system can overlap each other in time and which operations cannot overlap each other in time. The ability to overlap in time might be due to technical limitations (python GIL), system limitations (single core processor) or it might be intentional (explicit locking), but that is the actual thing you need to understand, and parallel vs concurrent just gives absolutely no information or insights whatsoever.
Here's how I know I'm right about this: Take any actual existing software or programming language or library or whatever, and describe it as parallel or concurrent, and then give the extra details about it that isn't captured in "parallel" and "concurrent". Then go back and remove any mention of "parallel" and "concurrent" and you will see that everything you need to know is still there, removing those terms didn't actually remove any information content.
Addition vs multiplication is a bad and useless thing to talk about. It's a waste of time. It's much more useful to talk about what number you get at the end. You might get that number from adding once, or twice, or even more times, but that final number is the actual thing you need to understand and "addition vs multiplication" just gives absolutely no information or insights whatsoever.
They're just different names for different things. Not caring that they're different things makes communication difficult. Why do that to people you intend to communicate with?
> If I launch 2 network requests from my async JavaScript and both are in flight then that’s concurrent.
That's because JS conflates the two. The async keyword in JavaScript queues things for the event loop which is running in a different thread, and progress will be made on them even if they are never awaited. In Rust, for example, nothing will happen unless those Futures are awaited.
It is an attempt to delineate some classes of concurrent & parallel programming challenges using a new term (really, redefining it in the programming context to be a bit tighter).
Oxford dictionary holds no relevance here, unless it has took over a definition from the field already (eg. look up "file": I am guessing it will have a computer file defined there) — but as it lags by default, it can't have specific definitions being offered.
The concepts of concurrency and parallelism are adjacent enough that they are often confused. A lot of languages provide basic concepts for both but use different frameworks for both. So the difference really matters in that case. Or the frameworks are just a bit low level and the difference really matters for that reason (because you need to think about and be aware of different issues).
I've been using Kotlin in the last few years. And while it is not without issues, their co-routines approach is a thing of beauty as it covers the whole of this space with one framework that is designed to do all of it and pretty well thought through. It provides a higher level approach in the form of structured concurrency, which is what Zig is dancing around here if I read this correctly (not that familiar with it so please correct if wrong) and not something that a lot of languages provide currently (Java, Javascript, Go, Rust, Python, etc.). Several of those have work in progress related to that though. I could see python going there now that they've bit the bullet with removing the GIL. But they have a bit of catching up to do. And several other languages provide ways that are similarly nice and sophisticated; and some might claim better.
In Kotlin, something being async or not is called suspending. Suspending just means that "this function sometimes releases control back to whatever called it". Typical moments when that happens are when it does evented IO and/or when it calls into other suspending functions.
What makes it structured concurrency is that suspend functions are executed in a scope, which has something called a dispatcher and a context (meta data about the scope). Kotlin enforces this via colored "suspend" functions. Calling them outside a coroutine scope is a compile error. Function colors are controversial with some. But they works and it's simple enough to understand. There's zero confusion on the topic. You'll know when you do it wrong.
Some dispatchers are single threaded, some dispatchers are threaded, and some dispatchers are green threaded (e.g. if on the JVM). In Kotlin, a coroutine scope is obtained with a function that takes a block as a parameter. That block receives its scope as a context parameter (typically 'this'). When the block exits, the whole tree of sub coroutines the scope had is guaranteed to have completed or failed. The whole tree is cancelled in case of an exception. Cancellation is one of the nasty things many other languages don't handle very well. A scope failure is a simple exception and if something cancelled, that's a CancellationException. If this sounds complicated, it's not that bad (because of Kotlin's DSL features). But consider it necessary complexity. Because there is a very material difference between how different dispatchers work. Kotlin makes that explicit. But otherwise, it kind of is all the same.
If inside a coroutine, you want to do two things asynchronously, you simply call functions like launch or async with another block. Those functions are provided by the coroutine scope. If you don't have one, you can't call those. That block will be executed by a dispatcher. If you want use different threads, you give async/launch an optional new coroutine scope with it's own dispatcher and context as a parameter (you can actually combine these with a + operator). If you don't provide the optional parameter, it simply uses the parent scope to construct a new scope on the fly. Structured concurrency here means that you have a nested tree of coroutines that each have their own context and dispatchers.
A dispatcher can be multi threaded (each coroutine gets its own thread) and backed by a thread pool, or a simple single threaded dispatcher that just lets each coroutine run until it suspends and then switches to the next. And if you are on the JVM where green thread pools look just like regular thread pools (this is by design), you can trivially create a green thread pool dispatcher and dispatch your co routines to a green thread. Note, this is only useful when calling into Java's blocking IO frameworks that have been adapted to sort of work with green threads (lots of hairy exceptions to that). Technically, green threads have a bit more overhead for context switching than Kotlin's own co-routine dispatcher. So use those if you need it; avoid otherwise unless you want your code to run slower.
There's a lot more to this of course but the point here is that the resulting code looks very similar regardless of what dispatchers you use. Whether you are doing things concurrently or in parallel. The paradigm here is that it is all suspend functions all the way down and that there is no conceptual difference. If you want to fork and join coroutines, you use functions like async and launch that return jobs that you can await. You can map a list of things to async jobs and then call awaitAll on the resulting list. That just suspends the parent coroutine until the jobs have completed. Works exactly the same with 1 thread or a million threads.
If you want to share data between your co-routines, you still need to worry about concurrency issues and use locks/mutexes, etc. But if your coroutine doesn't do that and simply returns a value without having side effects on memory (think functional programming here), things are quite naturally thread safe and composable for structured concurrency.
There are a lot of valid criticisms on this approach. Colored functions are controversial. Which I think is valid but not as big of a deal in Kotlin as it is made out to be. Go's approach is simpler but at the price of not dealing with failures and cancellation as nicely. All functions are the same color. But that simplicity has a price (e.g. no structured concurrency). And it kind of shovels paralellism under the carpet. And it kind of forces a lot of boiler plate on users by not having proper exceptions and job cancellation mechanisms. Failures are messy. It's simple. But at a price.
I think it's a great idea to not have to have two libraries - so its a "tick" from me for any idea that permits it.
The thing that bothers me in general about asynchronous code is how you test it so that you know with some confidence that if it passes the tests today you have replicated all the scenarios/orderings that might happen in production.
You have this same problem with threads of course and I've always found multithreaded programs to be much harder to write and debug....such that I personally use threading only when I feel I have to.
The actual problem with it is that caution is communicating it to developers. I recently had to work on a python system where the developers were obviously doing Javascript half the time. So ... hooray.... they put out a huge changeset to make the thing async....and threaded. Oddly enough none of them had ever heard of the GIL and I got the feeling of being seen as an irritating old bastard as I explained it to their blank stares. Didn't matter. Threading is good. Then I pointed out that their tests were now always passing no matter if they broke the code. Blank stares. They didn't realise that mangum forced all background tasks and async things to finish at the end of an HTTP request so their efforts to shift processing to speed up the response were for nothing.
Knowing things doesn't always matter if you cannot get other people to see them.
We plan to have in Zig a testing `Io` implementation that will potentially use fuzzing to stress test your code under a concurrent execution model.
That said, I think a key insight is that we expect most of the library code out there to not do any calls to `io.async` or `io.asyncConcurrent`. Most database libraries for example don't need any of this and will still contain simple synchronous code. But then that code will be able to be used by application developers to express asynchrony at a higher level:
Which makes things way less error prone and simpler to understand than having async/await sprinkled all over the place.That resonates. Testing asynchronous and multithreaded code for all possible interleavings is notoriously difficult. Even with advanced fuzzers or concurrency testing frameworks, you rarely gain full confidence without painful production learnings.
In distributed systems, it gets worse. For example, when designing webhook delivery infrastructure, you’re not just dealing with async code within your service but also network retries, timeouts, and partial failures across systems. We ran into this when building reliable webhook pipelines; ensuring retries, deduplication, and idempotency under high concurrency became a full engineering problem in itself.
That’s why many teams now offload this to specialized services like Vartiq.com, which handles guaranteed webhook delivery with automatic retries and observability out of the box. It doesn’t eliminate the async testing problem within your own code, but it reduces the blast radius by abstracting away a chunk of operational concurrency complexity.
Totally agree though – async, threading, and distributed concurrency all amplify each other’s risks. Communication and system design caution matter more than any syntax or library choice.
> That’s why many teams now offload this to specialized services like Vartiq.com
It would be nice to add a disclaimer that this is a system you're working on.
Defining async is hard. And I'm writing this as one of the many people who designed async in JavaScript.
I don't quite agree with the definition in this post: just because it's async doesn't mean that it's correct. You can get all sorts of user-land race conditions with async code, whether it uses `async`/`await` (in languages that need/support it) or not.
My latest formulation (and I think that it still needs work) is that async means that the code is explicitly structured for concurrency.
I wrote some more about the topic recently: https://yoric.github.io/post/quite-a-few-words-about-async/ .
The important thing to me is to distinguish between the abstract concept of asynchronism and how it can be implemented, and by the latter I mean both at the abstract level of a programming language and by technical coordination means in a machine. For the abstract concept at the highest level, well, it is just the dual of synchronism: two (or more) parties that somehow need to work together (i.e., one has a dependency on the other in a way that certain things need to happen before another one can continue) are not synchronized, meaning that it is not known or not defined when the things that need to happen after something else will be done. Seen that way, this definition is not hard. The hard thing can be the abstract means designed in a language: the amount of cognitive effort it takes in order to comprehended them and/or use them (in an fault-free way).
Largely agreed: async means that the code is structured in such a manner that the only way to be certain that a task is complete is to perform some rendez-vous. By extension, it covers mechanisms to make this happen in your code.
I am not deep into the matter but I would have given the answer: Async code is making code that would have been blocking non-blocking in a manner other stuff can still happen while it is being completed.
Since I work a lot in embedded loops where long running blocking snippets could literally break your I/O, lead to visible/audible dropouts etc. this would be the obvious answer.
But that's the thing: async, by itself, doesn't guarantee that anything is non-blocking. For your fiber (or any other kind of user-land abstraction) to be non-blocking, you MUST ensure that it doesn't perform any blocking call.
All async does is give you (some of) the tools to make code non-blocking.
Yeah, sure I mean in embedded-land any async snippet could perform any number of things, like firing a delay command that puts the whole processor to sleep.
This could potentially be avoided by clever enough compilers or runtimes, but I am not sure whether that would really be benefitial.
I am a fan of making things explicit, so the closer peoples idea of what aync is and what it isn't matches reality the better. Alternatively we should get the definition of what async should be clear first and then make the adjustment to the abstractions so they give us the guarantees people would natuarally assume come with that.
Yeah, I'm insisting because I recently reviewed a PR with async code calling blocking code, which made the entire exercise pointless. And that was from an experienced dev.
There used to be a few compilers that used static analysis to predict the cost of a call (where the cost of I/O was effectively considered infinite) and in which you could enforce that a branch only had a budget of N. Modern architectures tend to mess up with any finite value of N, but you could fairly easily adapt such techniques to detect unbounded values.
cries in Python asyncio
sympathizes
If you look on the bright side, it looks like free-threading is approaching, and OCaml has demonstrated how, by removing the GIL and adding exactly one primitive, you can turn a powerful enough language into a concurrency/parallelism powerhouse with minimal user-visible changes!
"Asynchrony" is a very bad word for this and we already have a very well-defined mathematical one: commutativity. Some operations are commutative (order does not matter: addition, multiplication, etc.), while others are non-commutative (order does matter: subtraction, division, etc.).
Usually, ordering of operations in code is indicated by the line number (first line happens before the second line, and so on), but I understand that this might fly out the window in async code. So, my gut tells me this would be better achieved with the (shudder) `.then(...)` paradigm. It sucks, but better the devil you know than the devil you don't.As written, `asyncConcurrent(...)` is confusing as shit, and unless you memorize this blog post, you'll have no idea what this code means. I get that Zig (like Rust, which I really like fwiw) is trying all kinds of new hipster things, but half the time they just end up being unintuitive and confusing. Either implement (async-based) commutativity/operation ordering somehow (like Rust's lifetimes maybe?) or just use what people are already used to.
> As written, `asyncConcurrent(...)` is confusing as shit, and unless you memorize this blog post, you'll have no idea what this code means. I get that Zig (like Rust, which I really like fwiw) is trying all kinds of new hipster things, but half the time they just end up being unintuitive and confusing. Either implement (async-based) commutativity/operation ordering somehow (like Rust's lifetimes maybe?) or just use what people are already used to.
I can't agree. It is confusing, because you need to remember the blog post, it wouldn't be confusing in the slightest if you internalized the core idea. The question remains: is it worth it to internalize the idea? I don't know, but what I do know is some people will internalize it and try to do a lot of shit with this in mind, and after a while we will be able to see where this path leads to. At that point we will be able to decide if it is a good idea or not.
> "Asynchrony" is a very bad word for this and we already have a very well-defined mathematical one: commutativity.
It is risky to use "commutativity" for this. Zig has operators, and some of them are commutative. And it will be confusing. Like if I wrote `f() + g(). Addition is commutative, then Zig is free to choose to run f() and g() in parallel. The order of execution and commutativity are different things. Probably one could tie them into one thing with commutative/non-commutative operators, but I'm not sure it is a good idea, and I'm sure that this is the completely different issue to experimenting with asynchrony.
I'm not sure they are that different, you could just as well store function calls in some constant each on its line then addition the result on a third. This is only syntax, not conceptual difference here. And on practical level, the difference is that the operator can be directly matched with some machine instruction, with operands being native data type such as integer.
Still, you might then prefer a word as permutability, or swappability.
Strictly speaking commutativity is defined over (binary) operations - so if one were to say that two async statements (e.g. connect/accept) are commutative, I would have to ask, "under what operation?"
Currently my best answer for this is the bind (>>=) operator (including, incidentally, one of its instances, `.then(...)`), but this is just fuzzy intuition if anything at all.
It's a good intuition. This has been studied extensively, the composition rule that is lax enough to permit arbitrary effects but strict enough to guarantee this class of outcomes is (>>=). We can keep trying to cheat this as long as we want, but it's bind.
Commutative operations (all of them I think?) are trivially generalized to n-ary operations (in fact, we do this via ∑ and ∏, in the case of addition and multiplication, respectively). You're right that the question of what "operation" we're dealing with here is a bit hazy; but I'd wager that it's probably in the family of the increment operation (N++ === N + 1 = 1 + N) since we're constantly evaluating the next line of code, like the head of a Turing machine.
Edit: maybe it's actually implication? Since the previous line(s) logically imply the next. L_0 → L_1 → L_2 → L_n? Though this is non-commutative. Not sure, it's been a few years since my last metalogic class :P
Implication sounds right. With no further analysis, running each line in order is correct (for whatever "order" is defined by a language, let's assume imperative).
A compiler could recognise that e.g. L_2 doesn't depend on L_1, and would be free to reorder them. And compilers do recognise this in terms of data dependence of operations.
Generalizing an associative binary op to an n-ary op just requires an identity element Id (which isn't always obvious, e.g. Id_AND=true but Id_OR=false).
Identity is nop / pass
> Generalizing an associative binary op to an n-ary op just requires an identity element Id (which isn't always obvious, e.g. Id_AND=true but Id_OR=false).
Only for n = 0, I think. Otherwise, generalizing associative binary f_2 to f_n for all positive integers n is easily done inductively by f_1(x) = x and f_{n + 1}(x_1, ..., x_n, x_{n + 1}) = f_2(f_n(x_1, ..., x_n), x_{n + 1}), with no need to refer to an identity. (In fact, the definition makes sense even if f_2 isn't associative, but is probably less useful because of the arbitrary choice to "bracket to the left.")
The "operator" in this case would be the CPU executing 2 or N procedures (or functions).
Commutivity is a very light weight pattern, and so is correctly applicable to many things, and at any level of operation, as long as the context is clear.
> "under what operation?"
You could treat the semicolon as an operator, and just like multiplication over matrices, it's only commutative for a subset of the general type.
Right, exactly. It's been said that (>>=) is a programmable semicolon.
[0] https://news.ycombinator.com/item?id=21715426
or carrots return/new line For that matter
`.then()` is ugly, `await` is pretty, but wouldn't the critical part to guarantee commutivity less than guaranteed order (in js) be the `Promise.all([])` part?
Asynchrony also allows for partial ordering. Two operations may still need to be retired in a particular order without having to execute in that order.
Subtraction for instance is not commutative. But you could calculate the balance and the deduction as two separate queries and then apply the results in the appropriate order.
> "Asynchrony" is a very bad word for this and we already have a very well-defined mathematical one: commutativity.
I don't think it's sufficient to say that just because another term defines this concept means it's a better or worse word. "commutativity" feels, sounds, and reads like a mess imo. Asynchrony is way easier on the palette
commutativity is also not correct, because 1) it means way more things than just temporal ordering and 2) there are cooky temporal ordering schemes you can come up with (interleaving multiple async/awaits in weird time-dependent ways) which aren't really describable in the simple mathematical notion of commutativity.
> So, my gut tells me this would be better achieved with the (shudder) `.then(...)` paradigm. It sucks, but better the devil you know than the devil you don't.
The whole idea behind `await` is to make the old intuition work without the ugliness of `.then()`. `f(); await g(); h()` has exactly the expected execution ordering.
Can confirm.
In JS, we designed `await` specifically to hide `.then()`, just as we had designed `.then()` because callbacks made tracking control flow (in particular errors) too complex.
How is that any better to have await? Any resources I might consult on this?
Well, one of the ways we "sold" async/await it to Google was by showing how we could improve Promise-based tests.
I recall that one of our test suites was tens of thousands of lines of code using `then()`. The code was complicated enough that these lines were by and large considered write-only, partly because async loops were really annoying to write, partly because error-handling was non-trivial.
I rewrote that test suite using `Task.spawn` (our prototype for async/await). I don't have the exact numbers in mind, but this decreased the number of LoC by a factor of 2-3 and suddenly people could see the familiar uses of loops and `try`/`catch`.
Well, consider the difference between
Compared to Even for this simple case I think it's much clearer. Then look at a more complex case: Now try re-writing this with then() and see the difference.Commutativity is a much weaker claim because one is totally before or after the other. e.g. AB may commute with C so ABC=CAB but it is not necessarily the case that this equals ACB. With asynchrony you are guaranteed ABC=ACB=CAB. (There may be an exisiting mathematical term for this but I don't know it)
You can prove three-term commutativity from two-term (I did it years ago, I think it looked something like this[1]), so the ordering doesn't matter.
[1] https://math.stackexchange.com/questions/785576/prove-the-co...
I'm not talking about a universe where all elements commute, I'm talking about a situation in which A, B, and C do not necessarily commute but (AB) and C do. For a rigorous definition: given X and Y from some semigroup G, say X and Y are asynchronous if for any finite decompositions X=Z_{a_1}Z_{a_2}...Z_{a_n} and Y=Z_{b_1}Z_{b_2}...Z_{b_m} (with Z's in G) then for any permutation c_1,...,c_{n+m} of a_1,...,a_n,b_1,...,b_m that preserves the ordering of a's and the ordering of the b's has XY=Z_{c_1}Z_{c_2}...Z_{c_{n+m}}. I make the following claim: if G is commutative then all elements are asynchronous, but for a noncommutative G there can exist elements X and Y that commute (i.e. XY=YX) but X and Y are not asynchronous.
To give a concrete example, matrix multiplication is not commutative in general (AB ≠ BA), but e.g. multiplication with the identity matrix is (AI = IA). So AIB = ABI ≠ BAI.
Or applied to the programming example, the statements:
123 = 312 ≠ 321.Strictly speaking this also requires associativity.
> Some operations are commutative (order does not matter: addition, multiplication, etc.)
Fun fact: order does matter for addition. (When adding many floating-point numbers with widely varying exponents.)
> Usually, ordering of operations in code is indicated by the line number
Except for loops which allow going backwards, and procedures which allow temporarily jumping to some other locally linear operation.
We have plenty of syntax for doing non-forwards things.
So "cooperative multitasking is not preemptive multitasking".
The typical use of the word "asynchronous" means that the _language is single-threaded_ with cooperative multitasking (yield points) and event based, and external computations may run concurrently, instead of blocking, and will report result(s) as events.
There is no point in having asynchrony in a multithreaded or concurrent execution model, you can use blocking I/O and still have progress in the program while that one execution thread is blocked. Then you don't need the yield points to be explicit.
While this is indeed the most common use, I'll bring as counter-examples Rust (or C#, or F#, or OCaml 5+) that supports both OS threads and async. OS threads are good for CPU-bound tasks, async for IO-bound tasks.
The main benefit of having async (or Go-style M:N scheduling) is that you can afford to launch as many tasks/fibers/goroutines/... as you want, as long as you have RAM. If you're using OS threads, you need to pool them responsively to avoid choking your CPU with context-switches, running out of OS threads, running out of RAM, etc. – hardly impossible, but if you're doing more than just I/O, you can run into interesting deadlocks.
> The main benefit of having async (or Go-style M:N scheduling) is that you can afford to launch as many tasks/fibers/goroutines/... as you want
Some have argued that the real solution to this problem is to "just" fix OS threads. Rumor has it Google has done exactly this, but keeps it close to their chest:
https://www.youtube.com/watch?v=KXuZi9aeGTw
https://lwn.net/Articles/879398/
Somewhat related and also by Google is WebAssembly Promise Integration, which converts blocking code into non-blocking code without requiring language support:
https://v8.dev/blog/jspi
I see a possible future where the "async/await" idea simply fades away outside of niche use-cases.
I kind of think the author simply pulled the concept of yielding execution out of the definition of concurrency and into this new "asynchrony" term. Then they argued that the term is needed because without it the entire concept of concurrency is broken.
Indeed so, but I would argue that concurrency makes little sense without the ability to yield and is therefore intrinsic to it. Its a very important concept but breaking it out into a new term adds confusion, instead of reducing it.
I'd count pure one-to-one parallelism as a form of concurrency that doesn't involve any yielding. But otherwise, I agree that all forms of non-parallel concurrency have to be yielding execution at some cadence, even if it's at the instruction level. (E.g., in CUDA, diverging threads in a warp will interleave execution of their instructions, in case one branch tries blocking on the other.)
>I kind of think the author simply pulled the concept of yielding execution out of the definition of concurrency and into this new "asynchrony" term.
Quote from the article where the exact opposite is stated:
> (and task switching is – by the definition I gave above – a concept specific to concurrency)
Well I'm having a hell of a time understanding what this article is trying to say. On a 3rd and 4th pass I think perhaps they mean task (in)dependency tracking is a fundamental concept. Independent tasks have "asynchrony." (Can we just say dependency and independency?)
But even with that definition, it seems like the idea of promises, task tracking, etc is well tread territory.
Then they conclude with how fire and forget tasks solve coloring but isn't that just the sync-over-async anti-pattern? I wouldn't be excited that my UI work stops to run something when there are no more green threads but they seem excited by it.
Anyway, I guess I got too distracted by the high concept "this is a fundamental change in thinking" fluff of the article.
Concurrency does not imply yielding...
Synchronous logic does imply some syncing and yielding could be a way to sync - which is what i expect you mean.
Asynchronous logic is concurrent without sync or yield.
Concurrency and asynchronous logic do not exist - in real form - in von Neumann machines
Asynchrony, in this context, is an abstraction which separates the preparation and submission of a request from the collection of the result.
The abstraction makes it possible to submit multiple requests and only then begin to inquire about their results.
The abstraction allows for, but does not require, a concurrent implementation.
However, the intent behind the abstraction is that there be concurrency. The motivation is to obtain certain benefits which will not be realized without concurrency.
Some asynchronous abstractions cannot be implemented without some concurrency. Suppose the manner by which the requestor is informed about the completion of a request is not a blocking request on a completion queue, but a callback.
Now, yes, a callback can be issued in the context of the requesting thread, so everything is single-threaded. But if the requesting thread holds a non-recursive mutex, that ruse will reveal itself by causing a deadlock.
In other words, we can have an asynchronous request abstraction that positively will not work single threaded;
1 caller locks a mutex
2 caller submits request
3 caller unlocks mutex
4 completion callback occurs
If step 2 generates a callback in the same thread, then step 3 is never reached.
The implementation must use some minimal concurrency so that it has a thread waiting for 3 while allowing the requestor to reach that step.
Completely agree. The server/client example in the post was just one example of a program not being able to make progress, you’ve just gave another which cannot be solved the same way, and I would bet there are many more that they will be discovering over time. IMO when async is used, concurrency needs to be ensured.
IMO the author is mixed up on his definitions for concurrency.
https://lamport.azurewebsites.net/pubs/time-clocks.pdf
Can you explain more instead of linking a paper? I felt like the definitions were alright.
> Asynchrony: the possibility for tasks to run out of order and still be correct.
> Concurrency: the ability of a system to progress multiple tasks at a time, be it via parallelism or task switching.
> Parallelism: the ability of a system to execute more than one task simultaneously at the physical level.
They're just different from what Lamport originally proposed. Asynchrony as given is roughly equivalent to Lamport's characterization of distributed systems as partially ordered, where some pairs of events can't be said to have occurred before or after one another.
One issue with the definition for concurrency given in the article would seem to be that no concurrent systems can deadlock, since as defined all concurrent systems can progress tasks. Lamport uses the word concurrency for something else: "Two events are concurrent if neither can causally affect the other."
Probably the notion of (a)causality is what the author was alluding to in the "Two files" example: saving two files where order does not matter. If the code had instead been "save file A; read contents of file A;" then, similarly to the client connect/server accept example, the "save" statement and the "read" statement would not be concurrent under Lamport's terminology, as the "save" causally affects the "read."
It's just that the causal relationship between two tasks is a different concept than how those tasks are composed together in a software model, which is a different concept from how those tasks are physically orchestrated on bare metal, and also different from the ordering of events..
The definition of asynchrony is bad. It's possible for asynchronous requests to guarantee ordering, such that if a thread makes two requests A and B in that order, asynchronously, they will happen in that order.
Asynchrony means that the requesting agent is not blocked while submitting a request in order to wait for the result of that request.
Asynchronous abstractions may provide a synchronous way wait for the asynchronously submitted result.
> The definition of asynchrony is bad. It's possible for asynchronous requests to guarantee ordering, such that if a thread makes two requests A and B in that order, asynchronously, they will happen in that order.
It's true that it's possible - two async tasks can be bound together in sequence, just as with `Promise.then()` et al.
... but it's not necessarily the case, hence the partial order, and the "possibility for tasks to run out of order".
For example - `a.then(b)` might bind tasks `a` and `b` together asynchronously, such that `a` takes place, and then `b` takes place - but after `a` has taken place, and before `b` has taken place, there may or may not be other asynchronous tasks interleaved between `a` and `b`.
The ordering between `a`, `b`, and these interleaved events is not defined at all, and thus we have a partial order, in which we can bind `a` and `b` together in sequence, but have no idea how these two events are ordered in relation to all the other asynchronous tasks being managed by the runtime.
I mean that it's possible in the sense of being designed in as a guarantee; that the async operations issued against some API object will be performed in the order in which they are submitted, like a FIFO queue.
I don't mean "promise.then", whereby the issuance of the next request is gated on the completion of the first.
An example might be async writes to a file. If we write "abc" at the start of the file in one request and "123" starting at the second byte in the second requests, there can be a guarantee that the result will be "a123", and not "abc2", without gating on the first request completing before starting the other.
async doesn't mean out of order; it means the request initiator doesn't synchronize on the completion as a single operation.
Concurrency is the property of a program to be divided into partially ordered or completely unordered units of execution. It does not describe how you actually end up executing the program in the end, such as if you wish to exploit these properties for parallel execution or task switching. Or maybe you're running on a single thread and not doing any task switching or parallelism.
For more I'd look up Rob Pike's discussions for Go concurrency.
The article understands this.
> Concurrency: the ability of a system to progress multiple tasks at a time, be it via parallelism or task switching.
Okay, but don't go with this definition.
Is the hang up on "at a time"? What if that were changed to something like "in a given amount of time"?
For single threaded programs, whether it is JS's event loop, or Racket's cooperative threads, or something similar, if Δt is small enough then only one task will be seen to progress.
Concurrency is parallelism and/or asynchrony, simply the superset of the other two.
Asynchrony means things happen out of order, interleaved, interrupted, preempted, etc. but could still be just one thing at a time sequentially.
Parallelism means the physical time spent is less that the sum of the total time spent because things happen simultaneously.
careful: in many programming contexts parallelism and concurrency are exclusive concepts, and sometimes under the umbrella of async, which is a term that applies to a different domain.
in other contexts these words don't describe disjoint sets of things so it's important to clearly define your terms when talking about software.
Yeah concurrency is not parallelism. https://go.dev/blog/waza-talk
That is compatible with my statement: Not all concurrency is parallelism, but all parallelism is concurrency.
What people mean by "concurrency is not parallelism" is that they are different problems. The concurrency problem is defining an application such that it has parts that are not causally linked with some other parts of the program. The parallelism problem is the logistics of actually running multiple parts of your program at the same time. If I write a well-formed concurrent system, I shouldn't have to know or care if two specific parts of my system are actually being executed in parallel.
In ecosystems with good distributed system stories, what this looks like in practice is that concurrency is your (the application developers') problem, and parallelism is the scheduler designer's problem.
yes, some people swap the meaning of concurrency and asynchrony. But, almost all implementations of async use main event loops, global interpreter lock, co-routines etc. and thus at the end of the day only do one thing at a time.
Therefore I think this definition makes the most sense in practical terms. Defining concurrency as the superset is a useful construct because you have to deal with the same issues in both cases. And differentiating asynchrony and parallelism makes sense because it changes the trade-off of latency and energy consumption (if the bandwidth is fixed).
Asynchrony also practically connotes nondeterminism, but a single-threaded concurrent program doesn't have to exhibit nondeterministic behavior.
> Asynchrony: the possibility for tasks to run out of order and still be correct.
Asynchrony is when things don't happen at the same time or in the same phase, i.e. is the opposite of Synchronous. It can describe a lack of coordination or concurrence in time, often with one event or process occurring independently of another.
The correctness statement is not helpful. When things happy asynchronously, you do not have guarantees about order, which may be relevant to "correctness of your program".
> The correctness statement is not helpful
But... that's everything, and why it's included.
Undefined behavior from asynchronous computing is not worth study or investment, except to avoid it.
Virtually all of the effort for the last few decades (from super-scalar processors through map/reduce algorithms and Nvidia fabrics) involves enabling non-SSE operations that are correct.
So yes, as an abstract term outside the context of computing today, asynchrony does not guarantee correctness - that's the difficulty. But the only asynchronous computing we care about offers correctness guarantees of some sort (often a new type, e.g., "eventually consistent").
The phrase "multiple tasks at a time" is ill-defined, according to Lamport, because whose clock are you trusting.
For lamport concurrent does not mean what it means to us colloquially or informally (like, "meanwhile"). Concurrency in Lamport's formal definition is only about order. If one task is dependent or is affected by another, then the first is ordered after the second one. Otherwise, they are deemed to be "concurrent", even if one happens years later or before.
Not the OP, but in formal definitions like Communicating Sequential Processes, concurrency means the possibility for tasks to run out of order and still be correct, as long as other synchronisation events happen
Concurrency implies asynchrony (two systems potentially doing work at the same time withut waiting for each other), but the converse is not true.
A single process can do work in an unordered (asynchronous) way.
Parallelism implies concurrency but not does not imply asynchrony.
> Asynchrony: the possibility for tasks to run out of order and still be correct.
Can't we just call that "independent"?
Not really. There may be some causal relations.
Can you give an example?
Locks, scheduling,... That introduce some synchronicity and so some kind of order. But it's enforced on the system and not a required mechanism.
But if I run "ls" on a machine, and another user runs "ls" on the same machine, wouldn't you consider them independent, even though the OS uses all kinds of locks and what not under the hood?
Doesn't multiple tasks at the same time make it simultaneous?
I think there needs to be a stricter definition here.
Concurrency is the ability of a system to chop a task into many tiny tasks. A side effect of this is that if the system chops all tasks into tiny tasks and runs them all in a sort of shuffled way it looks like parallelism.
This is why I've completely stopped using the term, literally everyone I talk to seems to have a different understanding. It no longer serves any purpose for communication.
Keep in mind that you can’t really express half the concepts in lamport’s papers in most languages. You don’t really talk about total and partial clock ordering when starting a thread. You only really do it in TLA+ when designing a protocol.
That being said, I agree we don’t need a new term to express “Zig has a function in the async API that throws a compilation error when you run in a non-concurrent execution. Zig let’s you say that.” It’s fine to so that without proposing new theory.
The author is aware that definitions exist for the terms he uses in his blog post. He is proposing revised definitions. As long as he is precise with his new definitions, this is fine. It is left to the reader to decide whether to adopt them.
He’s repurposing asynchrony that’s different from the way most literature and many developers use it, and that shift is doing rhetorical work to justify a particular Zig API split.
No thanks.
The new Zig I/O idea seems like a pretty ingenious idea, if you write mostly applications and don't need stackless coroutines. I suspect that writing libraries using this style will be quite error-prone, because library authors will not know whether the provided I/O is single or multi-threaded, whether it uses evented I/O or not... Writing concurrent/async/parallel/whatever code is difficult enough on its own even if you have perfect knowledge of the I/O stack that you're using. Here the library author will be at the mercy of the IO implementation provided from the outside. And since it looks like the IO interface will be a proper kitchen sink, essentially an implementation of a "small OS", it might be very hard to test all the potential interactions and combinations of behavior. I'm not sure if a few async primitives offered by the interface will be enough in practice to deal with all the funny edge cases that you can encounter in practice. To support a wide range of IO implementations, I think that the code would have to be quite defensive and essentially assume the most parallel/concurrent version of IO to be used.
It will IMO also be quite difficult to combine stackless coroutines with this approach, especially if you'd want to avoid needless spawning of the coroutines, because the offered primitives don't seem to allow expressing explicit polling of the coroutines (and even if they did, most people probably wouldn't bother to write code like that, as it would essentially boil down to the code looking like "normal" async/await code, not like Go with implicit yield points). Combined with the dynamic dispatch, it seems like Zig is going a bit higher-level with its language design. Might be a good fit in the end.
It's quite courageous calling this approach "without any compromise" when it has not been tried in the wild yet - you can claim this maybe after 1-2 years of usage in a wider ecosystem. Time will tell :)
> It will IMO also be quite difficult to combine stackless coroutines with this approach
Maybe there will be unforeseen problems, but they have promised to provide stackless coroutines; since it's needed for the WASM target, which they're committed to supporting.
> Combined with the dynamic dispatch
Dynamic dispatch will only be used if your program employs more than one IO implementation. For the common case where you're only using a single implementation for your IO, dynamic dispatch will be replaced with direct calls.
> It's quite courageous calling this approach "without any compromise" when it has not been tried in the wild yet.
You're right. Although it seems quite close to what "Jai" is purportedly having success with (granted with an implicit IO context, rather than an explicitly passed one). But it's arguable if you can count that as being in the wild either...
> I think that the code would have to be quite defensive and essentially assume the most parallel/concurrent version of IO to be used.
Exactly, but why would anyone think differently when the goal is to support both synchronous and async execution?
However, if asynchrony is done well at the lower levels of IO event handler, it should be simple to implemcent by following these principles everywhere — the "worst" that could happen is that your code runs sequentially (thus slower), but not run into races or deadlocks.
I think I'm missing something here, but the most interesting piece here is how would stackless coroutines work in Zig?
Since any function can be turned into a coroutine, is the red/blue problem being moved into the compiler? If I call:
Is that a function call? Or is that some "struct" that gets allocated on the stack and passed into an event loop?Furthermore, I guess if you are dealing with pure zig, then its fine, but if you use any FFI, you can potentially end up issuing a blocking syscall anyways.
I hope this is not a bad answer as I tried to understand what stackless coroutines even are for the past week.
1. Zig plans to annotate the maximum possible stack size of a function call https://github.com/ziglang/zig/issues/23367 . As people say, this would give the compiler enough information to implemented stackless coroutines. I do not understand well enough why that’s the case.
2. Allegedly, this is only possible because zig uses a single compilation unit. You are very rarely dealing with modules that are compiled independently. If a function in zig is not called, it’s not compiled. I can see how this helps with point 1.
3. Across FFI boundaries this is a problem in every language. In theory you can always do dumb things after calling into a shared library. A random C lib can always spawn threads and do things the caller isn’t expecting. You need unsafe blocks in rust for the same reason.
4. In theory, zig controls the C std library when compiling C code. In some cases, if there’s only one Io implementation used for example, zig could replace functions in the c std library to use that io vtable instead.
Regardless, I kinda wish kristoff/andrew went over what stackless coroutines are (for dummies) in an article at some point. I am unsure people are talking about the same thing when mentioning that term. I am happy to wait for that article until zig tries to implement that using the new async model.
The author does not seem to have made any non-trivial projects with asynchronicity.
All the pitfalls of concurrency are there - in particular when executing non-idempotent functions multiple times before previous executions finish, then you need mutexes!
Mutexes are part of the Io interface in zig. So is sleep, select, network calls, file io, cancellation…
> All the pitfalls of concurrency are there [in async APIs]
This is one of those "in practice, theory and practice are different" situations.
There is nothing in the async world that looks like a parallel race condition. Code runs to completion until it deterministically yields, 100% of the time, even if the location of those yields may be difficult to puzzle out.
And so anyone who's ever had to debug and reason about a parallel race condition is basically laughing at that statement. It's just not the same.
> Code runs to completion until it deterministically yields
No, because async can be (quote often is) used to perform I/O, whose time to completion does not need to be deterministic or predictable. Selecting on multiple tasks and proceeding with the one that completes first is an entirely ordinary feature of async programming. And even if you don't need to suffer the additional nondeterminism of your OS's thread scheduler, there's nothing about async that says you can't use threads as part of its implementation.
And I repeat, if you think effects from unpredictable I/O completion order constitute equivalent debugging thought landscapes to hardware-parallel races, I can only laugh.
Yes yes, in theory they're the same. That's the joke.
You're not reporting yourself, though, since you didn't make it clear at all in your initial comment you were talking about hardware. The previous comment you made only mentioned "parallel data races" in a conversation about software ecosystems, where both of those terms are regularly used to describe things that occur. You're laughing about dunking on people who you've run up to in the middle of a football field; no one stopped you from scoring because you didn't tell them that you're apparently playing an entirely different game on your own.
You don't need mutex in async code, since there is no parallel execution whatsoever. In fact languages that use async programming as a first class citizen (JavaScript) don't even have a construct to do them.
If you need to synchronize stuff in the program you can use normal plain variables, since it's guaranteed that your task will be never interrupted till you give control back to the scheduler by performing an await operation.
In a way, async code can be used to implement mutex (or something similar) themself: it's a technique that I use often in JavaScript, to implement stuff that works like a mutex or a semaphores with just promises to syncronize stuff (e.g. you want to be sure that a function that itself does async operations inside is not interrupted, it's possible to do so with promises and normal JS variables).
> You don't need mutex in async code, since there is no parallel execution whatsoever. In fact languages that use async programming as a first class citizen (JavaScript) don't even have a construct to do them.
This isn't even remotely true; plenty of languages have both async and concurrency, probably more than ones that don't. C# was the language that originated async/await, not JavaScript, and it certainly has concurrency, as do Swift, Python. Rust, and many more. You're conflating two independent proprieties of JavaScript as language and incorrectly inferring a link between them that doesn't actually exist.
https://developer.mozilla.org/en-US/docs/Web/API/LockManager...
Please don't implement this yourself
See this is what the OP is getting at, this is only true for async implementations that don't have any parallelism. That doesn't have to be the case, there's no reason that your javascript runtime couldn't take
and execute them in two threads transparently for you. It just happens, like the Python GIL, that it doesn't. Your JS implementation actually already has mutexes because web workers with shared memory bring true parallelization along with the challenges that come with.In the case of javascript, it's only allowed to do that when you can't detect it, situations where foo doesn't affect the output of bar. So as far as pitfalls are concerned it does one thing at a time. The rest is a hidden implementation detail of the optimizer.
Excellent article. I'm looking forward to Zig's upcoming async I/O.
It's kind of true...
I can do a lot of things asynchronously. Like, I'm running the dishwasher AND the washing machine for laundry at the same time. I consider those things not occurring at "the same time" as they're independent of one another. If I stood and watched one finish before starting the other, they'd be a kind of synchronous situation.
But, I also "don't care". I think of things being organized concurrently by the fact that I've got an outermost orchestration of asynchronous tasks. There's a kind of governance of independent processes, and my outermost thread is what turns the asynchronous into the concurrent.
Put another way. I don't give a hoot what's going on with your appliances in your house. In a sense they're not synchronized with my schedule, so they're asynchronous, but not so much "concurrent".
So I think of "concurrency" as "organized asynchronous processes".
Does that make sense?
Ah, also neither asynchronous nor concurrent mean they're happening at the same time... That's parallelism, and not the same thing as either one.
Ok, now I'll read the article lol
I think asynchronous as meaning out-of-sync, implies that there needs to be synchronicity between the two tasks.
In that case, asynchronous just means the state that two or more tasks that should be synchronized in some capacity for the whole behavior to be as desired, is not properly in-sync, it's out-of-sync.
Then I feel there can be many cause of asynchronous behavior, you can be out-of-sync due to concurent execution or due to parallel execution, or due to buggy synchronization, etc.
And because of that, I consider asynchronous programming as the mechanisms that one can leverage to synchronize asynchronous behavior.
But I guess you could also think of asynchronous as doesn't need to be synchronized.
Also haven't read the article yet lol
> I consider those things not occurring at "the same time" as they're independent of one another.
What would it take for you to consider them as running at the same time then?
That eventually the server and the client are able to connect.
The dishwasher and washing machine?
The way I like to think about it is that libraries vary in which environments they support. Writing portable libraries that work in any environment is nice, but often unnecessary. Sometimes you don't care if your code works on Windows, or whether it works without green threads, or (in Rust) whether it works without the standard library.
So I think it's nice when type systems let you declare the environments a function supports. This would catch mistakes where you call a less-portable function in a portable library; you'd get a compile error, indicating that you need to detect that situation and call the function conditionally, with a fallback.
I don't get it - the "problem" with the client/server example in particular (which seems pivotal in the explanation). But I am also unfamiliar with zig, maybe that's a prerequisite. (I am however familiar with async, concurrency, and parallelism)
Example 1
You can write to one file, wait, and then write to the second file.
Concurrency not required.
Example 2
You can NOT do Server.accept, wait, and then do Client.connect, because Server.accept would block forever.
Concurrency required.
Oh, I see. The article is saying that async is required. I thought it was saying that parallelism is required. The way it's written makes it seem like there's a problem with the code sample, not that the code sample is correct.
The article later says (about the server/client example)
> Unfortunately this code doesn’t express this requirement [of concurrency], which is why I called it a programming error
I gather that this is a quirk of the way async works in zig, because it would be correct in all the async runtimes I'm familiar with (e.g. python, js, golang).
My existing mental model is that "async" is just a syntactic tool to express concurrent programs. I think I'll have to learn more about how async works in zig.
I think a key distinction is that in many application-level languages, each thing you await exists autonomously and keeps doing things in the background whether you await it or not. In system-level languages like Rust (and presumably Zig) the things you await are generally passive, and only make forward progress if the caller awaits them.
This is an artifact of wanting to write async code in environments where "threads" and "malloc" aren't meaningful concepts.
Rust does have a notion of autonomous existence: tasks.
I think that notion is very specific to Rust's design.
Golang for example doesn't have that trait, where the user (or their runtime) must drive a future towards completion by polling.
Right, but Go is an application-level language and doesn't target environments where threads aren't a meaningful concept. It's more an artifact of wanting to target embedded environments than something specific to Rust.
Thanks, this clears things up for me.
I suppose I conflated "asynchrony" (as defined in the article) and "async" as a syntax feature in languages I'm familiar with.
Thank you so much for the added context. This explains the article very well.
But why is this a novel concept? The idea of starvation is well known and you don't need parallelism for it to effect you already. What does zig actually do to solve this?
Many other languages could already use async/await in a single threaded context with an extremely dumb scheduler that never switches but no one wants that.
I'm trying to understand but I need it spelled out why this is interesting.
The novel concept is to make it explicit where a non-concurrent scheduler is enough (async), and where it is not (async-concurrent). As a benefit, you can call async functions directly from a synchronous context, which is not possible for the usual async/await, therefore avoiding the need to have both sync and async versions of every function.
And with green threads, you can have a call chain from async to sync to async, and still allow the inner async function to yield through to the outer async function. This keeps the benefit of async system calls, even if the wrapping library only uses synchronous functions.
There's a great old book on this if someone wants to check it: Communicating Sequential Processes. From Hoare. Go channels and the concurrent approach was inspired on this.
I also wrote a blog post a while back when I did a talk at work, it's Go focused but still worth the read I think.
[0] https://bognov.tech/communicating-sequential-processes-in-go...
One thing that most languages are lacking is expressing lazy return values. -> await f1() + await f2() and to express this concurently requres manually handing of futures.
you mean like?
Although more realistically But also, futures are the expression of lazy values so I'm not sure what else you'd be asking for.This is what i hand in mind whit "manually handing of futures". In this case you have to write
I think this is just too much of synthactic noise.On the other hand, it is necessary becase some of underlying async calls can be order dependend.
for example
checks that first received socket byte is A and second is B. This is clearly order dependant that can't be executed concurrently out of order.Which languages do have such a thing?
Rust does this, if you don’t call await on them. You can then await on the join of both.
Is the "join" syntax part of the language?
Why is having it be syntax necessary or beneficial?
One might say "Rust's existing feature set makes this possible already, why dedicate syntax where none is needed?"
(…and I think that's a reasonably pragmatic stance, too. Joins/selects are somewhat infrequent, the impediments that writing out a join puts on the program relatively light… what problem would be solved?
vs. `?`, which sugars a common thing that non-dedicated syntax can represent (a try! macro is sufficient to replace ?) but for which the burden on the coder is much higher, in terms of code readability & writability.)
no
https://doc.rust-lang.org/std/future/macro.join.html
Then it doesn’t apply in this case.
I suppose Haskell does, as `(+) <$> f1 <*> f2`.
In there is also ApplicativeDo that works nicely with this.
this is evaluated as applicative in same way.That's because f2's result could depend on whether f1 has executed.
Blocking async code is not async. In order for something to execute "out of order", you must have an escape mechanism from that task, and that mechanism essentially dictates a form of concurrency. Async must be concurrent, otherwise it stops being async. It becomes synchronous.
Consider:
From the perspective of the application programmer, readA "block" readB. They aren't concurrent. In this example, the two operations are interleaved and the reads happen concurrently. The author makes this distinction and I think it's a useful one, that I imagine most people are familiar with even if there is no name for it.I think that confuses paradigm with configuration. Say, if one thread waits on another to finish, that doesn't mean the code suddenly becomes "single-threaded", it just means your two threads are in a serialized configuration in that instance. Similarly, when async code becomes serialized, it doesn't cease to be async: the scaffolding to make it concurrent is there, it's just unused in that specific configuration.
For example, C# uses this syntax:
when you have these two lines, the first I/O operation still yields control to a main executor during `await`, and other web requests can continue executing in the same thread while "readA()" is running. It's inherently concurrent, not in the scope of your two lines, but in the scope of your program.Is Zig any different?
This is exactly what the article is trying to debunk.
If you need to do A and then B in that order, but you're doing B and then A. It doesn't matter if you're doing B and then A in a single thread, the operations are out of sync.
So I guess you could define this scenario as asynchronous.
So wait, is the word they mean by asynchrony actually the word "dependency"?
> So wait, is the word they mean by asynchrony actually the word "dependency"?
No, the definition provided for asynchrony is:
>> Asynchrony: the possibility for tasks to run out of order and still be correct.
Which is not dependence, but rather independence. Asynchronous, in their definition, is concurrent with no need for synchronization or coordination between the tasks. The contrasted example which is still concurrent but not asynchronous is the client and server one, where the order matters (start the server after the client, or terminate the server before the client starts, and it won't work correctly).
> Which is not dependence, but rather independence
Alright, well, good enough for me. Dependency tracking implies independency tracking. If that's what this is about I think the term is far more clear.
> where the order matters
I think you misunderstand the example. The article states:
> Like before, *the order doesn’t matter:* the client could begin a connection before the server starts accepting (the OS will buffer the client request in the meantime), or the server could start accepting first and wait for a bit before seeing an incoming connection.
The one thing that must happen is that the server is running while the request is open. The server task must start and remain unfinished while the client task runs if the client task is to finish.
I'm about to reply to the author because his article is actually confusing as written. He has contradictory definitions relative to his examples.
> The contrasted example which is still concurrent but not asynchronous is the client and server one
Quote from the post where the opposite is stated:
> With these definitions in hand, here’s a better description of the two code snippets from before: both scripts express asynchrony, but the second one requires concurrency.
You can start executing Server.accept and Client.connect in whichever order, but both must be running "at the same time" (concurrently, to be precise) after that.
Your examples and definitions don't match then.
If asynchrony, as I quoted direct from your article, insists that order doesn't matter then the client and server are not asynchronous. If the client were to execute before the server and fail to connect (the server is not running to accept the connection) then your system has failed, the server will run later and be waiting forever on a client who's already died.
The client/server example is not asynchronous by your own definition, though it is concurrent.
What's needed is a fourth term, synchrony. Tasks which are concurrent (can run in an interleaved fashion) but where order between the tasks matters.
> If the client were to execute before the server and fail to connect (the server is not running to accept the connection) then your system has failed, the server will run later and be waiting forever on a client who's already died.
From the article:
> Like before, the order doesn’t matter: the client could begin a connection before the server starts accepting (the OS will buffer the client request in the meantime), or the server could start accepting first and wait for a bit before seeing an incoming connection.
When you create a server socket, you need to call `listen` and after that clients can begin connecting. You don't need to have already called `accept`, as explained in the article.
Not necessarily, but I guess it depends how you define "dependency".
For example, it might be partial ordering is needed only, so B doesn't fully depend on A, but some parts of B must happen after some parts of A.
It also doesn't imply necessarily that B is consuming an output from A.
And so on.
But there is a dependency yes, but it could be that the behavior of the system depends on both of them happening in some partial ordering.
The difference is with asynchronous, the timing doesn't matter, just the partial or full ordering. So B can happen a year after A and it would eventually be correct, or at least within a timeout. Or in other words, it's okay if other things happen in between them.
With synchronous, the timings tend to matter, they must happen one after the other without anything in-between. Or they might even need to happen together.
It's more accurate to say that callbacks and async/await can facilitate concurrency.
I think there's not much point trying to define these concepts as there is no consensus about what they mean. Different people have clear ideas about what each concept means but they just don't agree.
It's like integration tests vs unit tests... Most developers think they have a clear idea about what each one means, but based on my experience there is very little consensus about where the line is between unit test vs integration test. Some people will say a unit test requires mocking or stubbing out all dependencies, others will say that this isn't necessary; so long as you mock out I/O calls... Others will say unit tests can make I/O calls but not database calls or calls which interface with an external service... Some people will say that if a test covers the module without mocking out I/O calls then it's not an integration test, it's an end-to-end test.
Anyway it's the same thing with asynchrony vs concurrency vs parallelism.
I think most people will agree that concurrency can potentially be achieved without parallelism and without asynchrony. For many people, asynchrony has the connotation that it's happening in the same process and thread (same CPU core). Some people who work with higher level languages might say that asynchrony is a kind of context switching (as it's switching context in the stack when callbacks at called or promises resolved) but system devs will say that context switching is more granular than that and not constrained to the duration of specific operations, they'll say it's a CPU level concept.
“Permission for concurrency is not an obligation for concurrency. Zig lets you explicitly permit-without-obligation, to support the design of libraries that are polymorphic over a/sync ‘function color’.”
> Concurrency refers to the ability of a system to execute multiple tasks through simultaneous execution or time-sharing (context switching)
Wikipedia had the wrong idea about microkernels for about a decade too, so ... here we are I guess.
It's not a _wrong_ description but it's incomplete...
Consider something like non-strict evaluation, in a language like Haskell. One can be evaluating thunks from an _infinite_ computation, terminate early, and resume something else just due to the evaluation patterns.
That is something that could be simulated via generators with "yield" in other languages, and semantically would be pretty similar.
Also consider continuations in lisp-family languages... or exceptions for error handling.
You have to assume all things could occur simultaneously relative to each other in what "feels like" interrupted control flow to wrangle with it. Concurrency is no different from the outside looking in, and sequencing things.
Is it evaluated in parallel? Who knows... that's a strategy that can be applied to concurrent computation, but it's not required. Nor is "context switching" unless you mean switched control flow.
The article is very good, but if we're going by the "dictionary definition" (something programming environments tend to get only "partially correct" anyway), then I think we're kind of missing the point.
The stuff we call "asynchronous" is usually a subset of asynchronous things in the real world. The stuff we treat as task switching is a single form of concurrency. But we seem to all agree on parallelism!
Non-blocking i/o isn't asynchrony and the author should know better. Non-blocking io is a building block of asynchronous systems -- it is not asynchony itself. Today's asynchronous programming did not exist when non-blocking I/O was implemented in Unix in the 80's.
Is there anything new in this article?
Perhaps not, but sometimes the description from a different angle helps somebody understand the concepts better.
I don't know how many "monad tutorials" I had to read before it all clicked, and whether it ever fully clicked!
Most gen ed articles on HN are not new ideas, just articles that could be pages from a textbook.
a core problem is that the term async itself is all sorts of terrible, synchronous usually means "happening at the same time", is not what is happening when you don't use `async`
its like the whole flammable/inflammable thing
> Asynchrony is not concurrency
This is what I tell my boss when I miss standups.
Asynchrony, parallelism, concurrency, and even deterministic execution (albeit as a degenerate case) are all just species of nondeterminism. Dijkstra and Scholten’s work on the subject is sadly under appreciated. And lest one thing this was ivory tower stuff, before he was a professor Dijkstra was a systems engineer writing operating systems on hilariously bad, by our standards, hardware.
The argument about concurrency != parallelism mentioned in this article as being "not useful" is often quoted and rarely a useful or informative, and it also fails to model actual systems with enough fidelity to even be true in practice.
Example: python allows concurrency but not parallelism. Well not really though, because there are lots of examples of parallelism in python. Numpy both releases the GIL and internally uses open-mp and other strategies to parallelize work. There are a thousand other examples, far too many nuances and examples to cover here, which is my point.
Example: gambit/mit-scheme allows parallelism via parallel execution. Well, kindof, but really it's more like python's multiprocess library pooling where it forks and then marshals the results back.
Besides this, often parallel execution is just a way to manage concurrent calls. Using threads to do http requests is a simple example, while the threads are able to execute in parallel (depending on a lot of details) they don't, they spend almost 100% of their time blocking on some socket.read() call. So is this parallelism or concurrency? It's what it is, it's threads mostly blocking on system calls, parallelism vs concurrency gives literally no insights or information here because it's a pointless distinction in practice.
What about using async calls to execute processes? Is that concurrency or parallelism? It's using concurrency to allow parallel work to be done. Again, it's both but not really and you just need to talk about it directly and not try to simplify it via some broken dichotomy that isn't even a dichotomy.
You really have to get into more details here, concurrency vs parallelism is the wrong way to think about it, doesn't cover the things that are actually important in an implementation, and is generally quoted by people who are trying to avoid details or seem smart in some online debate rather than genuinely problem solving.
The difference is quite useful and informative. In fact, most places don't seem to state it strongly enough: Concurrency is a programming model. Parallelism is an execution model.
Concurrency is writing code with the appearance of multiple linear threads that can be interleaved. Notably, it's about writing code. Any concurrent system could be written as a state machine tracking everything at once. But that's really hard, so we define models that allow single-purpose chunks of linear code to interleave and then allow the language, libraries, and operating system to handle the details. Yes, even the operating system. How do you think multitasking worked before multi-core CPUs? The kernel had a fancy state machine tracking execution of multiple threads that were allowed to interleave. (It still does, really. Adding multiple cores only made it more complicated.)
Parallelism is running code on multiple execution units. That is execution. It doesn't matter how it was written; it matters how it executes. If what you're doing can make use of multiple execution units, it can be parallel.
Code can be concurrent without being parallel (see async/await in javascript). Code can be parallel without being concurrent (see data-parallel array programming). Code can be both, and often is intended to be. That's because they're describing entirely different things. There's no rule stating code must be one or the other.
Async JS code is parallel too. For example, await Promise.all(...) will wait on multiple functions at once. The JS event loop is only going to interpret one statement at a time, but in the meantime, other parts of the computer (file handles, TCP/IP stack, maybe even GPU/CPU depending on the JS lib) are actually doing things fully in parallel. A more useful distinction would be, the JS interpreter is single-threaded while C code can be multithreaded.
I can't think of anything in practice that's concurrent but not parallel. Not even single-core CPU running 2 threads, since again they can be using other resources like disk in parallel, or even separate parts of the CPU itself via pipelining.
> A more useful distinction would be, the JS interpreter is single-threaded while C code can be multithreaded.
...this seems like a long way round to say "JS code is not parallel while C code can be parallel".
Or to put it another way, it seems fairly obvious to me that parallelism is a concept applied to one's own code, not all the code in the computer's universe. Other parts of the computer doing other things has nothing to do with the point, or "parallelism" would be a completely redundant concept in this age where nearly every CPU has multiple cores.
When you define some concepts, those definitions and concepts should help you better understand and simplify the descriptions of things. That's the point of definitions and terminology. You are not achieving this goal, quite the opposite in fact, your description is confusing and would never actually be useful in understanding, debugging, or writing software.
Stated another way: if we just didn't talk about concurrent vs parallel we would have exactly the same level of understanding of the actual details of what code is doing, and we would have exactly the same level of understanding about the theory of what is going on. It's trying to impose two categories that just don't cleanly line up with any real system, and it's trying to create definitions that just aren't natural in any real system.
Parallel vs concurrent is a bad and useless thing to talk about. It's a waste of time. It's much more useful to talk about what operations in a system can overlap each other in time and which operations cannot overlap each other in time. The ability to overlap in time might be due to technical limitations (python GIL), system limitations (single core processor) or it might be intentional (explicit locking), but that is the actual thing you need to understand, and parallel vs concurrent just gives absolutely no information or insights whatsoever.
Here's how I know I'm right about this: Take any actual existing software or programming language or library or whatever, and describe it as parallel or concurrent, and then give the extra details about it that isn't captured in "parallel" and "concurrent". Then go back and remove any mention of "parallel" and "concurrent" and you will see that everything you need to know is still there, removing those terms didn't actually remove any information content.
Addition vs multiplication is a bad and useless thing to talk about. It's a waste of time. It's much more useful to talk about what number you get at the end. You might get that number from adding once, or twice, or even more times, but that final number is the actual thing you need to understand and "addition vs multiplication" just gives absolutely no information or insights whatsoever.
They're just different names for different things. Not caring that they're different things makes communication difficult. Why do that to people you intend to communicate with?
edit: fixed by the author.
yes I'm agreeing with the article completely
maybe change 'this' to a non-pronoun? like "rob pikes argument"
updated, thanks for the feedback
thanks for clarifying!
That’s word games.
If I launch 2 network requests from my async JavaScript and both are in flight then that’s concurrent.
Definition from Oxford Dictionary adjective 1. existing, happening, or done at the same time. "there are three concurrent art fairs around the city"
> If I launch 2 network requests from my async JavaScript and both are in flight then that’s concurrent.
That's because JS conflates the two. The async keyword in JavaScript queues things for the event loop which is running in a different thread, and progress will be made on them even if they are never awaited. In Rust, for example, nothing will happen unless those Futures are awaited.
It is an attempt to delineate some classes of concurrent & parallel programming challenges using a new term (really, redefining it in the programming context to be a bit tighter).
Oxford dictionary holds no relevance here, unless it has took over a definition from the field already (eg. look up "file": I am guessing it will have a computer file defined there) — but as it lags by default, it can't have specific definitions being offered.
The concepts of concurrency and parallelism are adjacent enough that they are often confused. A lot of languages provide basic concepts for both but use different frameworks for both. So the difference really matters in that case. Or the frameworks are just a bit low level and the difference really matters for that reason (because you need to think about and be aware of different issues).
I've been using Kotlin in the last few years. And while it is not without issues, their co-routines approach is a thing of beauty as it covers the whole of this space with one framework that is designed to do all of it and pretty well thought through. It provides a higher level approach in the form of structured concurrency, which is what Zig is dancing around here if I read this correctly (not that familiar with it so please correct if wrong) and not something that a lot of languages provide currently (Java, Javascript, Go, Rust, Python, etc.). Several of those have work in progress related to that though. I could see python going there now that they've bit the bullet with removing the GIL. But they have a bit of catching up to do. And several other languages provide ways that are similarly nice and sophisticated; and some might claim better.
In Kotlin, something being async or not is called suspending. Suspending just means that "this function sometimes releases control back to whatever called it". Typical moments when that happens are when it does evented IO and/or when it calls into other suspending functions.
What makes it structured concurrency is that suspend functions are executed in a scope, which has something called a dispatcher and a context (meta data about the scope). Kotlin enforces this via colored "suspend" functions. Calling them outside a coroutine scope is a compile error. Function colors are controversial with some. But they works and it's simple enough to understand. There's zero confusion on the topic. You'll know when you do it wrong.
Some dispatchers are single threaded, some dispatchers are threaded, and some dispatchers are green threaded (e.g. if on the JVM). In Kotlin, a coroutine scope is obtained with a function that takes a block as a parameter. That block receives its scope as a context parameter (typically 'this'). When the block exits, the whole tree of sub coroutines the scope had is guaranteed to have completed or failed. The whole tree is cancelled in case of an exception. Cancellation is one of the nasty things many other languages don't handle very well. A scope failure is a simple exception and if something cancelled, that's a CancellationException. If this sounds complicated, it's not that bad (because of Kotlin's DSL features). But consider it necessary complexity. Because there is a very material difference between how different dispatchers work. Kotlin makes that explicit. But otherwise, it kind of is all the same.
If inside a coroutine, you want to do two things asynchronously, you simply call functions like launch or async with another block. Those functions are provided by the coroutine scope. If you don't have one, you can't call those. That block will be executed by a dispatcher. If you want use different threads, you give async/launch an optional new coroutine scope with it's own dispatcher and context as a parameter (you can actually combine these with a + operator). If you don't provide the optional parameter, it simply uses the parent scope to construct a new scope on the fly. Structured concurrency here means that you have a nested tree of coroutines that each have their own context and dispatchers.
A dispatcher can be multi threaded (each coroutine gets its own thread) and backed by a thread pool, or a simple single threaded dispatcher that just lets each coroutine run until it suspends and then switches to the next. And if you are on the JVM where green thread pools look just like regular thread pools (this is by design), you can trivially create a green thread pool dispatcher and dispatch your co routines to a green thread. Note, this is only useful when calling into Java's blocking IO frameworks that have been adapted to sort of work with green threads (lots of hairy exceptions to that). Technically, green threads have a bit more overhead for context switching than Kotlin's own co-routine dispatcher. So use those if you need it; avoid otherwise unless you want your code to run slower.
There's a lot more to this of course but the point here is that the resulting code looks very similar regardless of what dispatchers you use. Whether you are doing things concurrently or in parallel. The paradigm here is that it is all suspend functions all the way down and that there is no conceptual difference. If you want to fork and join coroutines, you use functions like async and launch that return jobs that you can await. You can map a list of things to async jobs and then call awaitAll on the resulting list. That just suspends the parent coroutine until the jobs have completed. Works exactly the same with 1 thread or a million threads.
If you want to share data between your co-routines, you still need to worry about concurrency issues and use locks/mutexes, etc. But if your coroutine doesn't do that and simply returns a value without having side effects on memory (think functional programming here), things are quite naturally thread safe and composable for structured concurrency.
There are a lot of valid criticisms on this approach. Colored functions are controversial. Which I think is valid but not as big of a deal in Kotlin as it is made out to be. Go's approach is simpler but at the price of not dealing with failures and cancellation as nicely. All functions are the same color. But that simplicity has a price (e.g. no structured concurrency). And it kind of shovels paralellism under the carpet. And it kind of forces a lot of boiler plate on users by not having proper exceptions and job cancellation mechanisms. Failures are messy. It's simple. But at a price.