# Case against “maybe \`async\`”

**URL:** <https://internals.rust-lang.org/t/case-against-maybe-async/20144>\
**Category:** language design\
**Created:** [January 5, 2024, 11:34pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144 "2024-01-05T23:34:35Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![khimru](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/khimru/32/11672_2.png) [@khimru](https://internals.rust-lang.org/u/khimru)\
**Post date:** [January 5, 2024, 11:34pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/1 "2024-01-05T23:34:35Z")

</div>

I was reading about [Rust plans for 2023](https://smallcultfollowing.com/babysteps/blog/2024/01/03/async-rust-2024/) (as outlined by Nico) and also about [questions raised by ZiCog](https://users.rust-lang.org/t/what-is-the-future-goal-of-async-rust/104470/55):

> Why do we have synchronous calls to OS services at all? Why do read() and friends hang up the caller for ages rather than return immediately and then indicate completion later, with some kind of signal or event? Or even crudely providing something that can be polled for completion?
> 
> How would our programming languages look if we never had synchronous system calls?
> 
> How would the code look that those compilers generated?
> 
> Without synchronous, blocking, system calls normal threading as we know it, pthreads in C, std::thread in Rust and many other may never have needed to exist. Would all languages then be async by default?

And then I did a tiny experiment: what would happen if we pretend, for a moment, that there are no difference between sync and `async` functions?

Yeah, `async` function return reference to function frame and make it possible to `poll` them to the completion (they are generators deep in heart, not a regular functions), but still, it's obvious how one may convert `async` function into sync one in an “extra naïve way”. So we can take something like this:

```rust
extern "C" {
    // Make sure calculated value goes “somewhere outside” and not optimized away
    fn my_c_function(x: *mut i32); 
}

pub async fn square_async(num: i32) {
    unsafe { my_c_function (&mut (num * num)) }
}

```

And then make it sync with an extra-naïve, super-dumb executor:

```rust
pub fn square_async_to_sync(num: i32) {
    let waker = Waker::noop();
    let mut cx = Context::from_waker(&waker);
    let mut fut = pin!(square_async(num));
    loop {
      if let Poll::Ready(res) = fut.as_mut().poll(&mut cx) {
        return res
      }
    }
}

```

And then we compile it, compare to synchronous version… and [we get the exact same machine code, of course](https://godbolt.org/z/n3EfqeMh4). Simple dead code elimination, nothing magical.

But then… what's the point of “maybe `async`” feature? It seriously looks like a solution in a search of a problem.

What we **really** need is to solve a completely different task from the same post: “_traits for being generic over runtimes_”.

If we can do that then we may provide a “naïve, synchronous, runtime”, postulate that attempt to use `await` with `async` function from normal, synchronous, function injects dummy loop with `noop` waker… and voila: done. Everything works. Tiny change to the compiler, tiny change to the language.

And I may not understand some deep implications but I couldn't see how any “maybe `async`” feature would be able to magically remove all these pollings and wake up machinery if I **wouldn't** be able to magically switch my function from [poll\_read](https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html#tymethod.poll_read) to [read](https://doc.rust-lang.org/nightly/std/io/trait.Read.html#tymethod.read) — and if I'm not mistaken then “maybe `async`” proposals **don't** include anything like that.

---

<div class="post-metadata">

**Author:** ![CAD97](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cad97/32/3460_2.png) [@CAD97](https://internals.rust-lang.org/u/CAD97)\
**Post date:** [January 6, 2024, 5:35am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/2 "2024-01-06T05:35:11Z")

</div>

So I guess your proposal is that we don't need "maybe async" at the type level, because we can document when implementations are actually sync and [`block_on`](https://docs.rs/futures/latest/futures/executor/fn.block_on.html) them (or [`now_or_never`](https://docs.rs/futures/latest/futures/future/trait.FutureExt.html#method.now_or_never) them, if you want to panic for an await instead of busy looping (like you do, which is the absolute worst thing to do) or using a lightweight but not nothing executor (like `block_on`)) instead?

It _works_ in the sense that if you only write correct code things work as they would with proper "maybe async" support, but it immediately crumbles into a giant silent footgun the moment any actual `async` work is done... or the moment your "not actually async" implementation is called from code that wants to be async and now is suddenly blocking the executor.

The point of trait generics is writing generic code. If you're solely trying to fit through an existing interface, "async in name only" can be a reasonable _internal, non-public implementation detail_ choice to make things work. But it's a _horrible_ choice for public API that's supposed to play well and integrate with other code. The point of "maybe async" is that being `async` isn't just the permission to `await` (which includes the scheduling a wakeup) but it's also a communication of intent; intent that polling doesn't take any significant amount of time and it definitely shouldn't be calling OS functionality to block the thread.

_If_ you have an environment where IO blocking doesn't exist, then everything "being `async`" is almost enough of a solution, sure. But Rust lives in an environment with blocking, `await` cancellation point visibility is critical to writing `unsafe` code because cancellation safety (leaking stack frames) is stricter than unwind safety (running stack destructors), busy looping is the absolute worst way to handle "polling" an underlying blocking resource (you _want_ to block/suspend execution instead of wasting electricity and starving out other threads), and it still doesn't handle actual compute loads starving out tasks (i.e. those structurally multiplexed via `select!` and/or `join!` and thus polled from the same thread instead of being `spawn`ed as independent tasks).

I do fundamentally believe that "everything async" is the better choice for a new "scripting" language. Go is kind of that. But as much as "systems" versus "scripting" is a false dichotomy, Rust is firmly on the systems side that strongly wants if not needs to be able to reason about `async` effects at a type system level.

---

<div class="post-metadata">

**Author:** ![dlight](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/dlight/32/8462_2.png) [@dlight](https://internals.rust-lang.org/u/dlight)\
**Post date:** [January 6, 2024, 8:47am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/3 "2024-01-06T08:47:18Z")

</div>

> [@khimru](#):
>
> If we can do that then we may provide a “naïve, synchronous, runtime”, postulate that attempt to use `await` with `async` function from normal, synchronous, function injects dummy loop with `noop` waker… and voila: done. Everything works. Tiny change to the compiler, tiny change to the language.

That's like dynamic typing: rather than detecting a programming mistake at compile time through the type system, compile anyway and let the code do whatever.

If this is done, I would expect this to at least raise a lint that is `deny` be default. Because it would be very error prone otherwise.

> [@CAD97](#):
>
> The point of "maybe async" is that being `async` isn't just the permission to `await` (which includes the scheduling a wakeup) but it's also a communication of intent; intent that polling doesn't take any significant amount of time and it definitely shouldn't be calling OS functionality to block the thread.

I may be misunderstanding, but what about tasks in async executors meant specifically for CPU bound work? Like Bevy's [`AsyncComputeTaskPool`](https://docs.rs/bevy/latest/bevy/tasks/struct.AsyncComputeTaskPool.html) and [`ComputeTaskPool`](https://docs.rs/bevy/latest/bevy/tasks/struct.ComputeTaskPool.html). In the spawned futures, since you do cpu-bound work between each await point, polling them is expensive.

Now, why would a CPU bound thread pool run a future, rather than a closure? Well, this is an easy way to enable calling async APIs, with no loss of generality (you may just not `await` anything if you don't want to). So if Rayon were written today, it could have been written as an async executor that ran CPU bound futures, rather than just taking closures. (Likewise, Bevy has [`ParallelIterator`](https://docs.rs/bevy/latest/bevy/tasks/trait.ParallelIterator.html) just like Rayon, but running on the async executor)

So most of time a future mean "this is probably IO bound and polling is supposed to be quite cheap" but other times it means "this is actually CPU bound, but it is encapsulated as a future so that we can easily call async APIs". (A further complication is that there is no threshold that makes a code go from IO bound to CPU bound, and it's quite possible that a nominally IO bound code has some anomalous input that makes it behave as CPU bound)

---

<div class="post-metadata">

**Author:** ![CAD97](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cad97/32/3460_2.png) [@CAD97](https://internals.rust-lang.org/u/CAD97)\
**Post date:** [January 6, 2024, 9:55am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/4 "2024-01-06T09:55:09Z")

</div>

Yeah, using `async` for CPU bound work is done, but it still runs into the pitfall that `select!`/`join!` style concurrency including a CPU bound task starves the other. It's generally better to use non-async contexts for CPU intensive work for that reason, using something like `ctx.runtime().local_block_on(async_task())` to "switch colors," but it's also generally easiest to do so just by being `async` from the start and keeping that potential footgun in mind (since you already should be when doing `async` anyway).

It's a directionality thing. For "`async` input," accepting `async { blocking_work() }` gracefully is a relaxation of domain, and thus adequately handled by documentation. For "`async` output," producing a task which blocks the thread when polled is an expansion of range, thus a (safe but nonetheless) violation of the implicit expectation. It's more of an issue when talking about `trait`s, thus an API surface which is expected to be generic over, of course. (It's just that `Future` is a vocabulary bit of generality, and not playing nice with `select!`/`join!` is unfortunate.)

There's of course no universal hard cutoff for when a `poll` is too CPU intensive. But a reasonable soft idea is "would show in a profiler." At that point, you've starved other tasks on the thread for a time period worth measurement granularity. The heuristic used for "cooperative preemption" (i.e. compiler inserted) is typically a potential yield at any control flow back edge.

---

<div class="post-metadata">

**Author:** ![khimru](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/khimru/32/11672_2.png) [@khimru](https://internals.rust-lang.org/u/khimru)\
**Post date:** [January 6, 2024, 12:15pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/5 "2024-01-06T12:15:57Z")

</div>

> [@CAD97](#):
>
> It _works_ in the sense that if you only write correct code things work as they would with proper "maybe async" support, but it immediately crumbles into a giant silent footgun the moment any actual `async` work is done... or the moment your "not actually async" implementation is called from code that wants to be async and now is suddenly blocking the executor.

But this is the exact same story as when you take function that is designed to work in Tokio and try to push it into `async-std`. Or, heck, even use code which assumes multithreading executor and you run it in Tokio test where it's not available. Like [in this topic here](https://users.rust-lang.org/t/how-to-run-async-from-sync-code/104837/3).

> [@CAD97](#):
>
> But it's a _horrible_ choice for public API that's supposed to play well and integrate with other code.

Maybe. But we don't really, have a choice. We **already** have that problem in Rust, right now, today. It's even in the Niko list under _traits for being generic over runtimes_ name.

The short summary: you are correct, of course, in saying that allowing to call just any random `async fn` is dangerous but **it's not different from calling normal `async` with wrong executor**.

And, even worse, “true” “maybe `async`” wouldn't magically solve that issue! You would still be able to convert “wrong” `async` feature into synchronous code!

> [@CAD97](#):
>
> Rust is firmly on the systems side that strongly wants if not needs to be able to reason about `async` effects at a type system level.

I agree, but “maybe `async`” is entirely wrong abstraction! When “maybe `async`” is **useful** (if we, somehow, may distinguish code designed to work with different executors) it's not helpful, if we couldn't do that then “maybe `async`” doesn't add anything interesting to the table.

The short version of my post is: **purely synchronous executor is not special**. You either have a way to keep “oil and water” separate (keep `Tokio` features out of `async-std` executor **and/or** keep “purely `async` code” from being executed in synchronous environment) or you don't have it.

Special-casing “pure synchronous code” is **not** helpful! Yes, problems that you are talking about are real, but “maybe `async`” **doesn't solve them** and if would find a way to solve them (would it be “trait transformers” or anything else) then it would stop being useful!

---

<div class="post-metadata">

**Author:** ![denis](https://avatars.discourse-cdn.com/v4/letter/d/5e9695/32.png) [@denis](https://internals.rust-lang.org/u/denis)\
**Post date:** [January 13, 2024, 9:20am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/6 "2024-01-13T09:20:21Z")

</div>

> [@khimru](#):
>
> I couldn't see how any “maybe `async` ” feature would be able to magically remove all these pollings and wake up machinery if I **wouldn't** be able to magically switch my function from [poll\_read](https://docs.rs/tokio/latest/tokio/io/trait.AsyncRead.html#tymethod.poll_read) to [read](https://doc.rust-lang.org/nightly/std/io/trait.Read.html#tymethod.read) — and if I'm not mistaken then “maybe `async`” proposals **don't** include anything like that

Why would you assume that? The [initial announcement](https://blog.rust-lang.org/inside-rust/2022/07/27/keyword-generics.html) of Keyword Generics Initiative even has an example of a read trait that works both in sync and async:

> [@](#):
>
> ```rust
> async<A> trait Read {
> async<A> fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
> async<A> fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { ... }
> }
> 
> /// Read from a reader into a string.
> async<A> fn read_to_string(reader: &mut impl Read * A) -> std::io::Result<String> {
> let mut string = String::new();
> reader.read_to_string(&mut string).await?;
> string
> }
> 
> ```

I'd say that they do have switchable read and other essential traits in their design.

---

<div class="post-metadata">

**Author:** ![khimru](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/khimru/32/11672_2.png) [@khimru](https://internals.rust-lang.org/u/khimru)\
**Post date:** [January 13, 2024, 6:07pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/7 "2024-01-13T18:07:33Z")

</div>

> [@denis](#):
>
> Why would you assume that?

Because I couldn't see any proposals to do that.

> [@denis](#):
>
> I'd say that they do have switchable read and other essential traits in their design.

Yes, but they don't include the important part: how to make sure **existing, already written code may use them without rewrite**.

And if their (pretty sensible, if you will ask me) solution to solve that issue is to rewrite all the `async` crates then the question immediately arises: why not adopt some other solution which may keep code written for different executors separate? Because that's badly needed, too, it's even in the Niko's list under _Traits for being generic over runtimes_ name (except it's, somehow, comes **after** “maybe `async`” there even if without these “maybe `async`” is, pretty much, useless).

Let me repeat what I already wrote: **purely synchronous executor is not special**.

**Any** solution which would ensure that you couldn't mix features designed to be executed with different executors would **automatically** make “maybe `async`” trivially implementable: just inoculate `async fn` code into the synchronous function that calls it and dead code eliminator would do the rest.

And as you correctly noted we would need these traits and would need to manually adopt them and do what's needed to deal with them… even without “maybe `async`” proposal.

If “maybe `async`”, by itself, doesn't solve **any** real problems and doesn't make any other problems easier to solve… then why so much activity is around that proposal?

Think about how would you use Tokio-based code and Embassy-based code in the same program and “maybe `async`” would just happen automatically if you have that.

---

<div class="post-metadata">

**Author:** ![josh](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/josh/32/5934_2.png) [@josh](https://internals.rust-lang.org/u/josh)\
**Post date:** [January 14, 2024, 12:24pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/8 "2024-01-14T12:24:45Z")

</div>

> [@khimru](#):
>
> how to make sure **existing, already written code may use them without rewrite**

Crates are _already_ going to need to rewrite to use the async traits in std, once they exist.

I can appreciate the concept of "what if everyone wrote all code using async, including synchronous code". I also think that would be widely perceived as a usability issue, and in any case, doesn't help the mountain of existing synchronous code. It'd be a monumental amount of churn.

---

<div class="post-metadata">

**Author:** ![khimru](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/khimru/32/11672_2.png) [@khimru](https://internals.rust-lang.org/u/khimru)\
**Post date:** [January 14, 2024, 3:12pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/9 "2024-01-14T15:12:27Z")

</div>

> [@josh](#):
>
> Crates are _already_ going to need to rewrite to use the async traits in std, once they exist.

Yes. And that's my point: if we would need to rewrite everything anyway then it's better to do that **once** , not **twice**.

> [@josh](#):
>
> I can appreciate the concept of "what if everyone wrote all code using async, including synchronous code".

No. It's the other way around: **if** we would write code using `async` **and** would make `async` because like `const` behaves (allow use of function in `async` context, but also allow use of that same function in sync context) then we wouldn't need any of that “maybe `async`” nonsense.

> [@josh](#):
>
> It'd be a monumental amount of churn.

Why that's not a problem for `const` functions?

---

<div class="post-metadata">

**Author:** ![SkiFire13](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/skifire13/32/7579_2.png) [@SkiFire13](https://internals.rust-lang.org/u/SkiFire13)\
**Post date:** [January 14, 2024, 5:18pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/10 "2024-01-14T17:18:50Z")

</div>

> [@khimru](#):
>
> **if** we would write code using `async` **and** would make `async` because like `const` behaves (allow use of function in `async` context, but also allow use of that same function in sync context)

This reasoning doesn't work nicely because `const` functions restrict the capabilities of the function (you can only use features that are available in `const` contexts, which are a subset of what normal functions can do), while `async` functions enlarge the set of capabilities (you can now `.await` `Future`s).

From the point of view of the caller, you can call `const` functions in both `const` functions and normal functions, so they are less restricting. On the `async` side that role is taken by normal sync functions, because you can always call them in `async` functions.

The problem with this is that calling blocking sync functions in `async` code is bad, or if it's not blocking often doesn't compose well (think for example of `Option::map`, the closure inside needs to be sync even if `map` is called in a `async` context).

"Just let `async` functions be callable from sync functions" is not gonna work. You showed an example where you can just `poll` it once, but that's going to work only for those `async` functions that `.await` nothing meaningful and could just be sync functions (i.e. the most useless ones!). For all the others this is not gonna work and you will need an actual executor in order to not busy loop.

---

<div class="post-metadata">

**Author:** ![khimru](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/khimru/32/11672_2.png) [@khimru](https://internals.rust-lang.org/u/khimru)\
**Post date:** [January 14, 2024, 7:48pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/11 "2024-01-14T19:48:28Z")

</div>

> [@SkiFire13](#):
>
> For all the others this is not gonna work and you will need an actual executor in order to not busy loop.

So what? We **already** have that problem that not all `async` functions are callable from all executors. What makes synchronous executor special and deseriving special language support?

> [@SkiFire13](#):
>
> You showed an example where you can just `poll` it once, but that's going to work only for those `async` functions that `.await` nothing meaningful and could just be sync functions (i.e. the most useless ones!).

Also the only ones that **makes sense** to call in synchronous context.

> [@SkiFire13](#):
>
> This reasoning doesn't work nicely because `const` functions restrict the capabilities of the function (you can only use features that are available in `const` contexts, which are a subset of what normal functions can do), while `async` functions enlarge the set of capabilities (you can now `.await` `Future`s).

Yes, but as we all know problem is deeper: we **already** can cause lots of trouble by trying to mix `async` code designed to be run with different executors.

Making it possible to use special dedicated `'static` executor doesn't make it neither better nor worse.

If anything we may want to have generic `async` keyword but it shouldn't be picking between `async` and not-`async`.

Rather I would say that was we want is somthing like `async<Tokio>`, `async<Embassy>` and `async<Sync>`. With the possibility of making that type parameter generic.

---

<div class="post-metadata">

**Author:** ![CAD97](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cad97/32/3460_2.png) [@CAD97](https://internals.rust-lang.org/u/CAD97)\
**Post date:** [January 14, 2024, 9:03pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/12 "2024-01-14T21:03:23Z")

</div>

To add, the usual concept that Rust encourages is that if a function uses/requires some resource (e.g. the Tokio executor), it should either reach it by some function argument or, if it's a global/ambient resource, lazily initialize it when first used. The most notable example of this is rayon's compute thread pool, which is a global resource that spins up the first time it's needed unless you've [manually initialized it beforehand](https://docs.rs/rayon/latest/rayon/struct.ThreadPoolBuilder.html#method.build_global). async\_std also follows this pattern. lazy\_static/once\_cell exist to make implementing it more straightforward.

I conjecture that Tokio doesn't do this because they consider it important both that it's possible to run multiple executors side-by-side and that you don't end up doing so (or even initializing the global runtime) unintentionally. It's a reasonable enough choice to make, especially since Tokio has abundant performance relevant configuration available. It's mainly just unfortunate that it's not easily apparent what functionality is tied to the executor, and it's not fully non-controversial; some Tokio-`async`-using libraries (especially those providing a blocking API) wrap their functionality in an as-needed lazily initialized runtime.

Additionally, being `async`-generic is different than being executor-generic. In a perfect world (syntax irrelevant), `async fn` would mean “I may `.await` in an executor-agnostic manner” and `async(tokio)` would mean “I may `.await` in a way that requires the Tokio executor” (and/or reactor). It's also worth noting that the primary reason `async` code becomes executor-specific is `spawn`. Dynamic dispatch to `spawn` is fine (the subsequent polling is necessarily dynamic already), if a bit involved (to support alloc-constrained environments\[1\]), something there's vague intents on doing (`Context` is separate from `Waker` to enable passing additional context), and meaningfully simpler than maybe-`async` or executor generics.

If it helps, you could think about `fn` as shorthand for `async(never) fn`. In fact, if “maybe `async`” uses syntax along the lines of `async<A>`, executor dependencies could be slotted right into that `A` as an option alongside never and agnostic. (But on the other hand, support for implicit / ambient resources is useful even in purely sync code.)

As a final note, while `now_or_never`ing a future which cannot return pending optimizes the same as being written sync when everything can be inlined, that's no longer the case when function calls aren't inlined for any reason, be that code size or just that it's from a different compilation unit. And `async` functions can't do direct recursion either.

* * *

1. To make `fn spawn(impl Future + 'static)` dynamic, the simplest answer is `fn spawn(Box<dyn Future>)`, but that enforces the use of `Box`-allocation. The dynamic implementation thus needs to look something more like (pseudocode) `fn spawn(&mut Option<dyn Future + Any>)` so the provider can take the value and allocate it however they want, including into type-specific machinery like with embassy. (The std panic machinery has something similar with `dyn BoxMeUp`.)

---

<div class="post-metadata">

**Author:** ![reifying](https://avatars.discourse-cdn.com/v4/letter/r/c68b51/32.png) [@reifying](https://internals.rust-lang.org/u/reifying)\
**Post date:** [February 10, 2024, 6:00pm UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/13 "2024-02-10T18:00:31Z")

</div>

The blog post [Let futures be futures](https://without.boats/blog/let-futures-be-futures/) seems to make a great argument against `maybe[async]`. IMHO, the points there are pretty reasonable.

---

<div class="post-metadata">

**Author:** ![CAD97](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cad97/32/3460_2.png) [@CAD97](https://internals.rust-lang.org/u/CAD97)\
**Post date:** [February 11, 2024, 3:54am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/14 "2024-02-11T03:54:39Z")

</div>

While I do agree with essentially all of boats' points — code doing non-trivial work (anything you wouldn't want to happen during a singular `Future::poll`) can't meaningfully be async generic — but a surprising volume of code\[1\] boils down to just reshaping and moving data from point A to point B. Such "pipeline" code is functionally pure except for whatever effects are involved in taking data from the source and in giving it to the destination. It's the same procedure whether it's in a `fn(impl Iterator<In>, impl Fn(Out))` or `async fn(impl Stream<In>, impl Sink<Out>)` shaped box.

But the real "solution" to this case is that such code shouldn't be BLUE or RED or GREEN; it can and should ideally be written in a "colorless" sans-IO form (e.g. `fn(&mut State, In) -> impl '_ + Iterator<Out>`). Interestingly, it comes down to essentially external iteration ("here's more data") versus internal iteration ("give me more data") again, as it often seems most big-picture design questions do.

An unfortunate side effect of the "anything could block and rarely advertises that" fact is `async` sometimes being misused for only "poll safe (won't block/starve your executor)" functionality instead of also "(potentially) utilizes `async` concurrency." Any guarantee of compatibility with the [`now_or_never`](https://docs.rs/futures/latest/futures/future/trait.FutureExt.html#method.now_or_never) executor is imo abusing the `async` qualifier. The use isn't completely without merit — as OP notes, it's is the straightforward way to implement "maybe `async`" — but one of the takeaways of "let futures be futures" is that `async` as a marker of concurrency paradigm is useful, and doing this dilutes that.

We don't need generalization over monadic effects in order for the monadic patterns (e.g. map/filter/reduce) to be available and useful on our monadish types (e.g. `Result`/`impl Iterator`/`impl Future`/etc). In fact, _not_ generalizing allows names to be more specialized. It's not the end of the world if monadic transformers need to be rewritten across the different monadic effects, as the combinators themselves aren't that complicated, so long as "domain logic" is written to be minimally effectfull and the glue code for applying it in a monadic pipeline is minimal, straightforward, and obviously self evident.

* * *

I do wonder if language support for resumable semicoroutines will help here, as it allows writing an impl with internal iteration but exposing an API for external polling. If you're only potentially `async` because of callbacks, not work you're doing yourself — the exact case "maybe `async`" is usefully desirable — then, modulo the impl details, a "library style" poll providing events to an outer event loop is, _generally_ speaking, a preferable API to the "framework style" inner loop with callbacks. It's just much more convenient to write internal iteration than state machines, thus the popularity of `async.await` sugar.

The "full sync world" equivalent would be putting the work loop into a worker thread and mutex switching between the parent and child thread via a blocking bidi message handoff channel. That _should_ even be able to support borrowing cross thread, since it's functionally synchronous... something to play with later, I guess.

* * *

1. There's roughly three "kinds" of code:

---

<div class="post-metadata">

**Author:** ![comex](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/comex/32/2587_2.png) [@comex](https://internals.rust-lang.org/u/comex)\
**Post date:** [February 15, 2024, 5:40am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/15 "2024-02-15T05:40:02Z")

</div>

> [@CAD97](#):
>
> code doing non-trivial APIs work (anything you wouldn't want to happen during a singular `Future::poll`) can't meaningfully be async generic

I disagree.

boats' post is largely focused on "intra-task concurrency". As they say themselves:

> So it is exactly these combinators for intra-task concurrency that readiness-based futures were designed to optimize.
> 
> As Rain has compellingly [argued](https://sunshowers.io/posts/nextest-and-tokio/#what-are-heterogenous-selects) in the past, “heterogeneous select is the point of async Rust.”

They then go on to talk about `reqwest`, which apparently uses async Rust to "more easily able to abstract over the difference between multiplexing requests to multiple connections with HTTP/1 and multiplexing requests to the same connection with HTTP/2."

So I cloned the `reqwest` repo and looked for instances of `select!`, `join!`, `merge!`, and `zip!`, as well as other related APIs like `futures_util::future::*`. I also recursed into `hyper`, `h2`, `h3`, and `quinn`, which together seem to provide the actual backend for HTTP requests.

What did I find? Well, most of the crates had no uses at all, excluding test and example code. The exception was `hyper`, which didn't use any of the macros, but did have some uses of `futures_util::future::select`.

Thus, those crates, far from demonstrating the unique abilities of async, are largely examples of code doing non-trivial work that _could_ be meaningfully async-generic. For the code that did use `future::select`, making it support both async and sync would probably require the sync version to use some kind of wrapper that creates threads for you. You might end up with a few threads per connection that don't really need to exist. But it would hardly be prohibitive.

From my perspective the biggest advantage of the async ecosystem is not intra-task concurrency but cancellation. I haven't looked into how these crates handle cancellation, but I think they do take advantage of straightforward async cancel propagation. While there are obvious sync analogues for channels and mutexes and to some extent even futures, there's no analog for cancellation. There _could_ be a norm in the sync world that all work should be cancellable, not in a crazy abrupt way like pthread cancellation, but with a way to request cancellation that causes all blocking operations to start returning errors. OS kernels tend to work this way, because they're nonstandard environments that can invent their own synchronization primitives, and because you need to be able to kill processes even if they're in the middle of some kernel function that's blocking on a condition or something. But in typical Rust environments, no such norm exists.

I can imagine that if there was investment in making maybe-async work – in other words, investment in making a suite of APIs that are fully compatible between async and sync mode rather than just mostly compatible – then perhaps a bubble in the sync ecosystem could arise where there _is_ a norm of cancellability. C++ recently added [`std::jthread`](https://en.cppreference.com/w/cpp/thread/jthread) and [`std::stop_token`](https://en.cppreference.com/w/cpp/thread/stop_token) which are trying to do something similar. When operating in such a bubble, you would have to deal with the risk of accidentally calling a blocking function that doesn't support cancellation, but that's no worse than the risk of accidentally calling a blocking function in async code.

...But that's probably just a dream. After all, if blocking OS APIs don't themselves support cancellation, then the Rust wrappers for those APIs couldn't support it either, at least not without making their implementations much more complex and probably slower. And at that point, it's easy to argue that you might as well go all the way to async.

Personally, I just can't be happy with that solution, because there are just too many dissatisfying things about Rust's async design that make me never want to use it (debugger support, Pin, ABI stability / FFI compatibility, compile times, generated code quality, ease of accidentally failing to poll futures). But I digress.

---

<div class="post-metadata">

**Author:** ![system](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/system/32/14092_2.png) [@system](https://internals.rust-lang.org/u/system)\
**Post date:** [May 15, 2024, 5:40am UTC](https://internals.rust-lang.org/t/case-against-maybe-async/20144/16 "2024-05-15T05:40:38Z")

</div>

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.
