Course contentsShow
← Rust
Lesson 1155 of 1,91010. Concurrency PrimitivesFree lesson

Abstracting Over Concurrency Runtimes

You will build modular code that remains agnostic to the underlying executor implementation.

In this lesson, you will learn how to design library code that remains agnostic to whether the user runs it on Tokio, async-std, or a custom embedded executor.

The Problem of Hardcoded Runtimes

When you write code that calls tokio::spawn or uses tokio::sync::Mutex, your library becomes "runtime-coupled." A user cannot easily swap out the executor or use your library in a lightweight environment that doesn't support the full Tokio stack. To build robust, reusable crates, we must abstract away the act of spawning tasks and blocking.

The Strategy: Trait-Based Injection

Instead of relying on global runtime functions, you define a trait that represents the specific concurrency capabilities your code requires. By requiring this trait as a generic bound, you shift the responsibility of execution to the library user.

Consider a component that needs to run a background task:

pub trait SpawnTask {
    fn spawn_task<F>(&self, future: F) 
    where 
        F: std::future::Future<Output = ()> + Send + 'static;
}

pub struct MyService<S: SpawnTask> {
    executor: S,
}

impl<S: SpawnTask> MyService<S> {
    pub fn start_work(&self) {
        self.executor.spawn_task(async {
            // Do concurrent work here...
        });
    }
}

Why This Matters

By using this pattern, you avoid hard-coding dependencies. A user could implement SpawnTask for a wrapper around tokio::spawn, or implement it for a simple std::thread::spawn wrapper in a smaller application.

This approach also simplifies testing. During unit tests, you can inject a "mock" executor that runs futures synchronously or inspects them, rather than requiring a complex multi-threaded runtime to spin up just to verify your logic. You gain total control over the execution environment without sacrificing the ergonomic benefits of async/await.

A Note on Compatibility

While this abstraction is powerful, be mindful of types that are tied to specific runtimes. For example, a tokio::sync::Mutex cannot be awaited on a different executor. When abstracting, ensure that your data structures are also runtime-agnostic—use standard library primitives or portable alternatives where possible to ensure your abstractions actually work across different backends.

Key Takeaway: Decouple your logic from the runtime by defining interfaces for concurrency primitives, allowing the end-user to inject their preferred executor at the boundary.