@radekmie

On Multisynchronicity

By Radosław Miernik · Published on · Comment on Reddit

Table of contents

Intro

The vast majority of the code that most programmers work with is executed synchronously. In this setting, every instruction1 is computed separately, one at a time, in the order defined by the language or the runtime.

However, most programs operate with a database, communicate with external services, or do some sort of background computation. All of these operations have some kind of inherent delay. Classically, the program will pause and wait for the result. Yes, it’ll block the UI as well.

But there’s another way! We could perform these operations asynchronously, i.e., schedule them as well as some code that will be executed once the result is ready. There are many ideas for implementing these APIs, with varying levels of abstractions and performance considerations.

Ideally, we would abstract the waiting away, but we can’t. Or can we?

Viral nature of async

Making a part of the existing codebase async may sound trivial – especially for the stakeholders – but it has immediate and severe consequences. Why? Because every other piece of code using it has to become async as well. If the architecture wasn’t “ready” for it, it will take tens or even hundreds of lines just to propagate this one change.

I think that the virality1 of async has been the single most critiqued feature in a long time. Especially now, when more and more languages introduced some sort of syntax-level support – JavaScript, Python, Rust

At the same time, I’d say that introducing the async keyword is the best thing that has happened to modern JavaScript. Actually, it made it modern, in a way. (See On JavaScript Ecosystem for more “modernizations”.)

Escape hatch

Some languages have some sort of an “async escape hatch”. For example, in Java, the Future.get method will block the execution until the result is ready. For extra convenience, there’s often another option to wait with a timeout.

If you ever implemented such “waiting” in a low-level language, you know that it’s not that simple. Especially, if you don’t want to implement “busy waiting”, i.e., waiting by repeatedly checking the condition. In some situations, it’s fine, but usually, it’s not, as it tends to max up the CPU while doing so.

On the other hand, there’s nothing like that in JavaScript; at least not by default. Meteor, a Node.js framework, relies on the fibers package to wrap the Promises, making it possible to “unwrap” it. Just keep in mind that this package has its downsides, and there’s an ongoing discussion on removing it.

Multisynchronicity in Nim

As I already said, being able to write isomorphic (in terms of synchronicity) code is our holy grail. Surprisingly, it’s already possible in Nim! The idea is simple: implement an async function and generate the sync one.

As said in the docs, the multisync pragma (macro) will do exactly that. When we look into the code, it literally strips down all awaits. If you’d like to see it in practice, check out this Implementing Redis Client tutorial.

Actually, async is a macro in Nim as well. What it does, is replace all of the awaits with the appropriate iterators; await is basically a yield + read(). (I had a blast while working with Nim because I always could jump to the code and see how these complex ideas – like parallel – are implemented.)

Multisynchronicity in JavaScript

Let’s think about how we could achieve the same level of simplicity in JavaScript. Of course, we could do the same transformation as multisync does, e.g., with a Babel plugin. But to make it work, we have to be able to actually wait.

Let’s take a step back and think about what we actually want. We’d like to have an option to maybe wait for a Promise to resolve and then continue processing. In other words, we’d like to have something like then that accepts bare values.

While rethinking the form validation flow in uniforms (#711), I wanted to do precisely that. Previously, even if the entire validation flow was synchronous, the result was a Promise – just because some parts could be async.

As a result, I created a then function that receives two arguments. The first one is either a value (of type T) or a Promise of it (Promise<T>). The second one is a function (T -> U), that maps the first argument. The implementation is trivial. Try it yourself: (x, f) => x instanceof Promise ? x.then(f) : f(x).

Once you have it, you can use it the same way as .then() on the Promise object. There are two significant differences: no error handling and the potential of unleashing the callback hell (once again). While the former could be solved by adding a try..catch block and a second function for the error flow, the latter is solvable only with code transformations (again, like a Babel plugin).

Please note that adding the error handling to it would bring it much closer to the Result type in Rust and other languages. Whether or not the Promises should “carry” their error types is another topic2.

Problem with await in JavaScript

All of this fuss, and we finally ended up with a Promise-like then function. Was it even worth it? Why won’t we stick to the await literally everywhere? Well, there’s a problem. A subtle but important one.

Because of the way how ECMAScript standard defined await, it adds some slight overhead delay. It’s barely noticeable but quickly accumulates – even in real-life scenarios. It got significantly better with this change, foreshadowed in this excellent writeup and released in V8 v7.2.

However, it’s still not ideal. For example, for await..of is significantly slower than for..of for synchronous iterators (nodejs/node#31979; especially this). That’s again a problem that may be solved by changing the standard, but such changes are relatively rare.

And last but not least, using await always creates new Promise objects. No, I won’t rant about the memory usage here, as the modern engines excel at handling that. The problem is much simpler: sometimes you’d like to branch off a Promise just to handle the error.

Closing thoughts

The idea of multisynchronicity is fairly simple yet basically non-existent in mainstream programming languages. But maybe it’s going to be the next big thing – just like async. I’m not so sure about it, but it’d be awesome.

As for now, let’s stick to async. It really makes the code easier to comprehend and maintain. The performance overhead is there, sure, but so are the benefits. It’s up to you to decide, but for me, it’s a no-brainer. It’s just worth it.

Write idiomatic code. Performance will come next. It always does.

3

An instruction doesn’t necessarily have to be an assembler one. In this context, it can be a statement, expression, or any kind of computation in your language.

1

If you’d like a more creative analogy, try coloring your functions red and blue.

2

In short: they won’t. If you’d like them to, just use a handmade Result type. Or use any of the hundreds of TypeScript packages that do it.