Coming from Python/gevent, I can share some experience with the thread on implicit await.
At first, implicit await is awesome. Suddenly, I no longer need to expend the cognitive energy to keep track of yield points; everything is just a normal function call. And yet, some of those functions implicitly yield (internally) and it makes developing concurrent microservices very ergonomic, indeed. When you want to explicitly yield, that’s also available via gevent.sleep(0.0)
But after using it for the greater part of a decade, implicit yield does have some caveats, and it is worth pointing them out. The most common pitfall is using blocking functions. Commonly from libraries and packages that either aren’t monkeypatched, or are actually written in C/C++ and simply cannot be monkeypatched to cooperate with the event loop. This problem almost always manifests as terrible performance in production, because once you stop carrying the mental baggage of concurrency, you generally don’t test for it, either.
The second pitfall we often drop into is with shared mutable state. Coroutines (and green threads, etc) are naturally data-race free, just like the Rust guarantee. But coroutines (and Rust itself) will not prevent race conditions with shared mutable state. The shared state can be mutated across yield points, just the same as it can be mutated across synchronous call sites. (I believe this was mentioned above by @rpjohnst) In this sense, the await
keyword won’t save you from the e.g. interior mutability of RefCell
any more than a gevent-patched implicitly-yielding function call does. So any argument for-or-against implicit await leaning on the assumption that one improves the situation with shared mutable state is, IMHO, moot and misleading.
The third most common pitfall is the learning curve when ramping up newcomers to an existing codebase. Because the concurrency is built-in and hidden, it is really non-obvious (and unintuitive!) how it actually works. It’s just a bunch of blackbox magic that happens whether or not you are aware of its existence at all. This of course often leads back to the first problem with pulling in new dependencies that do not play nicely with the concurrency primitives. (The code functions perfectly, it just silently stops cooperating in random places.) And subsequently, a lot of time is spent debugging these kinds of issues.
For a concrete example of the latter case, we recently introduced the pika client for RabbitMQ into a service written with gevent. The client implements a “blocking” connection, which when monkeypatched by gevent, everything seems to work fine out of the gate. The issue crops up later when an idle connection is silently dropped until the next time a message is published on a topic; You get a connection reset exception! That’s because pika’s blocking API expects to drive its I/O loop so it can respond to server heartbeats. When the server doesn’t receive timely responses, it closes the connection. But the application was written so that the green thread would be mostly blocked on a separate consuming socket, starving the pika blocking API from driving its I/O loop. The solution was to spawn an I/O loop driver in a new green thread.
The illustration here is that just because it looks like it works, in reality that doesn’t always mean that it works as expected. I am under the impression that explicit await
would have prevented this kind of condition (or at least been a good indicator of something gone awry) because it is nonsensical to await
a synchronous function (like the blocking client API in pika). And if you know you are blocking a single threaded application, then the locality of the bug becomes more apparent.
Ok, so I’ve droned on for long enough about my experience with implicit await. Like I said, it’s very attractive, and I empathize with everyone who agrees with that sentiment. But at the same time, I’m skeptical of implied yields being considerably better than the await
keyword. At the same time, function coloring is also a burden, so I understand the other half of the motivation for implicit await. But that issue is distinct in itself, and would be a conversation about removing or greatly improving Futures. Or as @MajorBreakfast put it, whether Futures are a good idea at all.