Implementing a sync trait with async methods

If I have a trait

trait Foo {
    fn bar(&self);
}

Let's assume bar needs to make a network request and I want it to be async. What is the best way of doing this?

I can't make bar an async function and use the same trait. I could use tokio::spawn_blocking in the caller. Or the struct implementing this trait could take a tokio::runtime::Handle and run async code on that, but the call itself would not be async. Or I could make an actor. Or another method I haven't thought of. Is there a consensus?

Not really, aside from the general rule of avoiding mixing your sync (blocking) code with the async (yielding back to the executor) code as much as possible.

Which is why you often get both sync and async versions of the most popular crates out there. You can do synchronous, blocking work, in the standard synchronous context, and async where you have a global executor doing its async tasks, switching automatically between them as needed.

If your whole code is synchronous and you want to slap some async networks request just because "you want it", in general, it's not considered a great idea. Just use a blocking call and wait for the result to come back - simple, straightforward, no mess of sync/async required.

If your program is running in a tokio runtime, on the other hand - then make sure there's absolutely no way you can substitute your sync trait for an async one, because things will become trivial with it. If not, pretty much any of your considerations will work. Which of them will be the easiest to implement / fastest in practice is a whole other topic, though.

Generally it's not really possible to do that if you also want to use bar from async code.

This doesn't directly answer your question, but there is a famous blog post on almost this exact topic: What Colour is your Function?.

The idea is that the sync and async worlds are two different beasts, and code written using one of them will often "infect" the code around it. This will either force you to align the neighbouring code with your sync/async code, or use unsatisfactory solutions to resolve the friction.

1 Like

Thanks all for your responses. That is as I suspected. The code in my project is otherwise running in an async (tokio) context, but a dependency I'm interested in is synchronous. I have read "What Colour is your Function?", it makes some good points.

I think for my use case, calling into the implementor of Foo::bar from spawn_blocking in tokio makes the most sense.

This is not the first synchronous crate that I've wanted to use from an async context. The previous one was notify, but I wrapped that in an actor spawned in a background thread.

1 Like

Ultimately that's the reality of using synchronous code from async code.

I wrote a crate called desync that provides a data structure that's essentially a replacement for both mutexes and threads. It has a bunch of functions for mixing and matching synchronous and asynchronous code. The future_desync() function could be used to perform your network request asynchronously in the background: call detach() on the returned future to leave it running. Anything that later needs the updated state of things synchronously could retrieve them with the sync() function, or there's a future_sync() function if you're able to await them instead (these functions also work in the other direction, to turn a synchronous API into an asynchronous one).

The main limitation is one that probably can't be entirely overcome: if you need the result of an operation synchronously, it's necessary to wait for the operation to complete (but then again, desync makes it easy to asynchronously wait for the synchronous result so this is perhaps not always a practical limitation)

Desync works by being strict about the order of operations: once an asynchronous operation has been queued on an object, any other operations will not run until after it has completed, so once an asynchronous operation is queued from a sychronous method, the object can be treated as if that operation has already completed (this strong ordering property makes it easy to reason about async operations and avoid introducing race conditions too).

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.