This doesn't seem as useful as it implies though with the proviso statement that you can wind up with deadlocks in sync mode that you wouldn't have in async mode. Sounds like more of a footgun you can hand yourself.
Very nice, but how does it support separate compilation? Does the compiler always compile two versions of each function or zig only support whole program compilation ( by JITing for example)?
Or is it just stackful coroutines and there is really no difference between async and sync functions?
I believe it's up to the programmer to implement this. Here's some code from the zig github[1] page that illustrates how it might look once zig is stable (this is from an issue).
pub fn write(file: *File, bytes: []const u8) usize {
// Note that this is an if with comptime-known condition.
if (std.event.loop.instance) |event_loop| {
// non blocking version
} else {
// blocking call
}
}
So I guess functions that don't support async could just do something like:
if (!std.event.loop.instance) @compileError("X function only supports async");
Thanks, but how is a waiter suspended? The typical await implementation will do a CPS conversion of the waiter function, but to trigger the conversion it needs to see an await call, or pessimistically generate two versions of each functions.
But from your example it seems that Zig just do context switching (not knocking it, I'm a big fan of it over await), but then why would it need an await/async keyword at all?
This is interesting, it seems like a global mode that kind of turns normal Zig code into Go or Erlang-like non-blocking code. It does mean you have to hold non-local context in your head while reading functions, but maybe that's a worthy tradeoff?
> This is interesting, it seems like a global mode that kind of turns normal Zig code into Go or Erlang-like non-blocking code
Nope! There's no language magic going on, and the global switch is just a global variable in the root module. It only affects a few functions in the standard library that check that variable.
Zig relies heavily on compile time code executing for metaprogramming, and your code can read symbols from the root source file via @import("root"). All that
pub const io_mode = .evented;
does is set a variable that the standard library then checks. When io_mode is set to .evented, the library sets up an event loop and makes async calls internally. When it isn't, it makes blocking calls as you would expect. Unless you explicitly use 'async' to call functions, then they're going to act as if they block as they will be immediately await-ed upon, so there's no change.
> Nope! There's no language magic going on, and the global switch is just a global variable in the root module. It only affects a few functions in the standard library that check that variable.
I didn’t say there was magic, I was describing the practical effect of this feature. On a type-theoretic level, it’s like entering a global async monad, which is roughly the structure that Go and Erlang have.
But since you mentioned it, global variables that have side effects are definitely magic, in my view. If I did this in Ruby by monkey-patching synchronous IO methods in response to a global flag, people would definitely call it magic. And I understand how the feature works, but my reservations about it remain.
As for results being immediately awaited meaning there’s no change, I think I disagree. What happens if another task fails and brings down the whole system while I’m awaiting something? It wouldn’t have been scheduled in if I was programming synchronously.
> On a type-theoretic level, it’s like entering a global async monad, which is roughly the structure that Go and Erlang have.
This is the most concise summary of what Zig actually does! Thank you.
I always felt it was a little off, but it makes sense when you think about it in terms of "Zig makes all of your functions red, even if you don't want them to be."
In fact, in the article linked above, it says this in the FAQ:
> Q: SO I DON’T EVEN HAVE TO THINK ABOUT NORMAL FUNCTIONS VS COROUTINES IN MY LIBRARY?
> No, occasionally you will have to. As an example, if you’re allowing your users to pass to your library function pointers at runtime, you will need to make sure to use the right calling convention based on whether the function is async or not. You normally don’t have to think about it because the compiler is able to do the work for you at compile-time, but that can’t happen for runtime-known values.
So in essence, Zig actually is still colored, but the compiler handles for you what it can.
>But since you mentioned it, global variables that have side effects are definitely magic, in my view. If I did this in Ruby by monkey-patching synchronous IO methods in response to a global flag, people would definitely call it magic.
It definitely is "magic", but it's one of the few areas where I welcome it, personally.
What other options are available? The three I know of are function-colored (JavaScript, Python), async/automatic context-switching by default (Go, Erlang), or monkeypatching to go from colored to async/automatic context-switching by default (Zig, Python gevent).
I also hate globals, side effects, and monkeypatches, but I find the monkeypatch method to be the best compromise. I still use gevent in pretty much every Python project, and have for the past 10 years.
I don't think there is really a "better" option than this. Personally I find it distasteful because (if I understand this feature correctly) it means I have to know whether somebody turned on the async flag in another file to fully understand the possible control flow of a function I'm reading. This is different from Erlang because we just assume that every line can be preempted, and different from async-await because seeing async-await means you don't know if the code is actually going to run asynchronously. It's not the choice I would have made, but I am a stickler for theoretical cohesiveness and local readability, and Zig is kind of an anti-theory language. I would have chosen function colors, in all likelihood.
I suppose user code can do the same thing, but still, it's kind of magic.
When people speak of magic in code, they mean something that looks innocent but has huge ramifications because some other part of the code (that you are not aware of) does special handling.
In this case, what looks like a simple variable declaration acts more like configuration, but it doesn't look like configuration.
It would be much less magical if instead the language required you to call something from main, for example:
That's an interesting design, it seems like the best of both worlds. Zip has really neat ideas. I wish it didn't go the "case as you want road" though.
> I wish it didn't go the "case as you want road" though.
That's almost inevitable given its desire for close interop with C (as in, the Zig compiler includes a C compiler and can directly #include C headers).
The standard library has a relatively consistent case style though, which the Zig guide recommends.
Kotlin's co-routines are pretty nice. Technically it uses colored functions but because it is statically compiled language, there is no chance of doing this wrong as that would simply be a compile error.
The way suspend functions work in Kotlin is that it is essentially syntactic sugar for async await where calling a suspend function implicitly awaits it; which you can only do in a so-called CoroutineScope (for example another suspend function).
It's actually very similar to what go does. Instead of go foo(), Kotlin makes you write something like CoroutineScope(CoroutineName("myscopename")).launch { foo() }.
It's actually very similar to what go does as well. Instead of go foo(), Kotlin makes you write something like CoroutineScope(CoroutineName("myscopename")).launch { foo() }
The difference is that a coroutine scope is a thing with a name and some behavior. For example, you can control how the coroutine is dispatched for example. Some dispatchers might do everything on the main thread. Other dispatchers use thread pools. Very useful if you are integrating some blocking IO framework where you don't want to block the main thread.
There's more to it but it's a pretty well designed solution for what is a tough problem space.
You can call an async from a non async function of course. You just can’t return it’s result synchronously.
Nitpick aside, I have got to the point where I care little! Every language has its boilerplatish things, be it Elm’s JSON parsing, GO’s “if (err != nil)” or Haskells myriad language extension declarations. I don’t think asyncs on a chain of functions, or converting a callback function into a promise based one is that bad.
The issue is if you need to add an async call to an existing synchronous code base. If you introduce async in this situation, you’ll probably need to rewrite a huge chunk of your code to allow it. To prevent inconsistencies, this leads to a practice where programmers return promises or tasks by default, even in synchronous functions; which can cause a myriad of problems beyond simply looking really ugly.
> The issue is if you need to add an async call to an existing synchronous code base.
What it seems is needed is a blue function that wraps a red function with (in the simple case, though the actual inplementation needs to be more complicated to identify when thr sinple case applies vs when you have to do something more complicated) a dedicated single-item event loop and returns its result [0] (in async-await, I’d want to have a keyword “sync” used much like “await” but in an otherwise synchronous context to wrap a red function in this blue function.) JS—or at least browsers specifically—might avoid having this, or the tools to do it, to avoid encouraging use of async constructs in a sync context which could kill UI liveness, but for other red/blue async/sync environments it would be useful to have, whether in function or keyword form.
In Rust, calling an async function from a sync function looks like this[0]:
block_on(myasyncfn())
This basically repeatedly polls the future returned by the async function, until it completes, and then returns it. (you can also call a full-featured executor like Tokio, but for simple stuff block_on is perfect)
My mental model is like this: just like you use await to wait for a future in an async context, you use block_on to wait for a future in a sync context.
You only need to call block_on on the top-level future, meaning that myasyncfn() can have a lot of futures underneath. The only thing that won't work is spawning other tasks (new top-level futures that can continue executing even after the future returned by the function completes). Which is actually amazing: after block_on returns, there is no async background tasks lurking on: the program just continues as a normal non-async program.
I don't know how async is in other languages, but I guess that every language has something equivalent to block_on? How on Earth you integrate async code into sync code otherwise?
As an aside, calling a potentially blocking sync function from an async function is easy too, like this[1]:
spawn_blocking(myblockingsyncfn)
This spawns a new OS thread (or reuse one from the threadpool dedicated to blocking operations), then run the provided function in there. spawn_blocking returns a future that, when awaited, waits for the function in the other thread to complete, and then returns its value.
In Python at least, you can easily call an async function from a sync function:
result = asyncio.run_until_complete(func)
Also, having explicit async/await are a wait to tell you: here are the point in your code that have something doing unreliable side effects down there.
You can then use this information to:
- move that code and group it, so you get the rest of the code sansIO style
- put that in a try/except, because this code WILL fail at some point
- think about the perfs of this part of your code, which is what you are doing async io the first place
I do wish that python coro would schedule themself automatically, like JS does, and return a future that you can await explicitly. But that ship as sailed.
You can do that in pretty much every language, it's just that in most of implementations, blocking on an async call can easily dead-lock, .NET is particularly notorious in this regard: you call GetResult(), the scheduler tries to schedule the task to the thread that have just called the GetResult() on it -- and this thread is now deadlocked, because it waits for itself. But since a threadpool has many threads, such degradation takes some time until the whole of the system deadlocks, and it's not even guaranteed to happen: the task scheduling in .NET is very convoluted.
I've been burned by this so many times that I just simply will never use async in any code that is beyond trivial complexity and/or has performance requirements attached. I'll recommend breaking your solution into small pieces that execute separately and pass data to one another via messaging, to isolate the "colors" from one another at the process level.
I'm not a huge fan of async/await (though I have to write a fair amount of it) but that's throwing the baby out with the bathwater. The way to avoid "sync-over-async" deadlocks is to not do sync-over-async, i.e. don't call blocking methods/properties like Wait() and Result on tasks from threadpool threads - it's not what they're for (though they have valid, necessary uses and therefore can't simply be removed).
I've never written code that's deadlocked in this way. I've fixed a ton of such deadlocks, though, and every one boils down to somebody not following this simple rule, and more generally not thinking clearly about what their multithreaded code will actually do when it runs. (Encouraging people to be sloppy in their thinking about these issues is one of the reasons I mildly dislike C#'s concurrency facilities - but nonetheless it's perfectly possible to write code with them that isn't riddled with deadlocks.)
> more generally not thinking clearly about what their multithreaded code will actually do when it runs
The whole reason people are trying to build better concurrent/parallel computational abstractions is because they're sick of having to think about that stuff. But to be able to stop thinking about it you need to arrive at abstractions that don't leak: that is, their failure modes should themselves be (non-leaky) abstractions of the lower-level error details. Async/await still forces you to learn about thread-pools and scheduling — while in Erlang one has to learn about scheduling only to optimize performance, and deadlocking requires one to explicitly write a "receive" without a timeout.
The different philosophies of "explicitly annotate async functions" vs "implicit async" vs "create os threads with polling" etc will always continue because they all have tradeoffs.
So, the "simplification" or a "unification" of a concurrency model always comes at a cost that some programmers don't want to pay. Whenever you see, "Language <X> has solved the color problem so it's not an issue." -- the reflexive skepticism should be, "At what price, and do I want to pay it?"
I think it's instructive that Rust had a clean-sheet start to design a new language and so far, they've rejected Go style "channels" as Rust core syntax and instead, adopted the async/await paradigm. You don't have to program in Rust to get the gist of the following previous comments about their thought process:
There are definitely tradeoffs. In javascript I can cavalierly code knowing that a sequence of lines will not be interrupted by another thread. (Unfortunately, I still have some mental overhead because I keep thinking, "This program will be screwed if they ever introduce multi-threading".)
Async implementations like Python's have roughly the same speed as Bernstein's tcpserver, which forks a process for every connection and as such has perfect isolation between connections.
There can be very specific exceptions. In C# Winforms code I wrote, I have data loading code which I made async, called from UI code that was not. You need a running messageloop which was provided by a modal progress bar. Users could cancel the async code from the dialog. In the real world it is a quite a typical example where you want to call async code from non-async.
One of the more interesting parts is also that "cps-async" works fine with arbitrary effect monads. There are integrations with Cats' IO and ZIO and some more.
I wish there were a statically typed language with Lua's stackful coroutines and the ability to manually yield. It seems that Lua and Ruby are the only two well-known languages with that kind of stackful coroutine. And also, LuaJIT has proven that they can be implemented in a performant way, so I'm not sure if performance is a valid obstacle to implementing them more often.
Even languages like Go and Java with proper fibers don't allow you to yield where you want to, and the yield points are inserted by the compiler/runtime instead.
A use case which I can't seem to reimplement without stackful coroutines is "inline" blocking input, as found in the source code of old roguelikes. It makes the game logic easier to follow since you don't have to rearchitect the entire engine to be event-based, and can instead yield between an "update" and "draw" coroutine each frame.
msg("Do you want your possessions identified?)
local yes = yes_or_no()
if yes then
identify_possessions()
end
I was honestly very surprised when I first realized that this is not how most graphical turn-based games are programmed.
I dislike all of Ruby's magic, metaprogramming, and that it's almost impossible to statically analyze, however I have come to appreciate how nice it is in Ruby not to have functions with 'color' like this article mentions. I certainly hope TC39 can find a way to bake in similar async-agnostic syntax to Javascript. I'm not sure if it's possible with syntax alone, or if it would require a new language.
Consider yourself fortunate to have (forgive the assumption!) never worked in a ruby codebase that uses eventmachine or promise.rb. It’s less common than in some other languages but ruby is perfectly capable of a callback/promise-based model for performing async io or query batching, for example. And it creates exactly the “function color” problem that this article describes.
Are these two fundamentally opposing viewpoints? Or can these two sets of ideas be reconciled somehow?
The article that I posted specifically calls out Java as an example of why you need explicit await, yet the article in the OP praises Java for not having it!
In previous threads I haven't seen an in-depth discussion of algebraic effects as a solution to this problem, i.e. composing arbitrarily-many 'red' functions. Thoughts?
I’m thinking your reference to composing red functions doesn’t seem congruent with your desire to discuss algebraic effects. Composing arbitrarily many red functions, or blue functions, is a solved problem. (I won’t mention the lingo knowing many in the audience to be allergic.) Composing functions with heterogenous, or completely arbitrary, colors is much less so. That’s what the article is about, too, is it not - combining blue and red? But I might well be missing something deeper here, perhaps you’ll enlighten the reader :)
I would be really keen on seeing a mainstream-ish language that does algebraic effects or that can even implement them in a legitimate sense (discounting React’s implementation here, for instance). Purescript tried, but are doing something different these days. https://purescript-resources.readthedocs.io/en/latest/eff-to...
By 'red' I was referring to anything effectful, whether that be concurrency, IO, state, or otherwise. Composing any one effect is a solved problem (e.g. monad); composing arbitrarily many effects is a lot harder. Recent languages like Koka describe effects over the free monad; because they are described over a single monad, and this single monad composes, effects compose.
I am by no means an expert in this area, but I hope this clarifies the intent of my original comment. What transformations and/or constraints exist for composable heterogeneous effects?
The danger is that declaring too much at the API boundary makes the problem worse. Declared effects result in more ways that API’s can be incompatible and fewer ways that an implementation can change without breaking compatibility.
I remember this post being referenced by Brian Goetz, which ultimately leaded to the development of project Loom in the JVM (green threads handled by the runtime)
I might be showing my inexperience here, but my understanding of async/await and promises in JS is that they create threads under the hood to solve the problem the author is talking about.
I'm afraid I just don't quite understand why async/await don't solve the problem and instead sweep it under the rug, could someone explain it?
> my understanding of async/await and promises in JS is that they create threads under the hood to solve the problem the author is talking about.
No, that is wrong in multiple ways. Javascript is fundamentally designed around an event loop processed by a single thread. The only multitreading facility it has are web workers, which don't share memory with the main thread.
> I'm afraid I just don't quite understand why async/await don't solve the problem and instead sweep it under the rug, could someone explain it?
Both are really only syntactic sugar over callbacks. Under the hood, async/await splits the await-ing function and passes the second part as a callback to the await-ed function. And it requires the await-ing function itself to be async.
Promises are just a nicer way to do callbacks. It's impossible to get a result from a promise (or an ansync function) synchronously. If there were a way, it would block the main thread and kill performance completely.
If you want to use ‘await’, the calling scope needs to be an async function, which means any other function which calls the now async caller also needs to be async (well, if you need the resolved value. You can just orphan promises but then you’ll probably have unintended consequences). You’re forced to propagate it up the chain, which one may argue is a leaky abstraction.
Nope, async-await isn’t a solution to the problem, its just a slightly less annoying syntax sugar for defining and calling “red” functions, to which everything in the article applies.
What Color Is Your Function? (2015) - https://news.ycombinator.com/item?id=23218782 - May 2020 (85 comments)
What Color is Your Function? (2015) - https://news.ycombinator.com/item?id=16732948 - April 2018 (45 comments)
What Color Is Your Function? - https://news.ycombinator.com/item?id=8984648 - Feb 2015 (143 comments)
What Color Is Your Function? - https://news.ycombinator.com/item?id=8982494 - Feb 2015 (3 comments)