Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lesson 24: Bridging Sync and Async — block_on, spawn_blocking, Handle

What you’ll learn

  • Calling async code from sync code (block_on, Handle::block_on)
  • Calling sync/blocking code from async code (spawn_blocking)
  • The blocking thread pool and its configuration
  • Common pitfalls and anti-patterns

Key concepts

Async from sync: block_on

#![allow(unused)]
fn main() {
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(async {
    fetch_data().await
});
}

block_on parks the current thread until the future completes. Never call it from inside an async context (deadlock).

Sync from async: spawn_blocking

#![allow(unused)]
fn main() {
let hash = tokio::task::spawn_blocking(move || {
    // CPU-heavy or blocking I/O — runs on dedicated thread pool
    compute_bcrypt_hash(&password)
}).await?;
}

The blocking pool has up to 512 threads by default. Tasks here do not block the async worker threads.

Handle for deferred async access

#![allow(unused)]
fn main() {
let handle = tokio::runtime::Handle::current();

std::thread::spawn(move || {
    // From a plain OS thread, run async code:
    handle.block_on(async {
        client.get(url).send().await
    });
});
}

Anti-patterns

Anti-patternProblemFix
block_on inside asyncDeadlockUse .await
Blocking in async taskStarves workersspawn_blocking
spawn_blocking for I/OWastes pool threadsUse async I/O
Nested runtimesPanicUse Handle::current()

Exercises

  1. Call an async HTTP client from a synchronous main using block_on
  2. Use spawn_blocking to offload a CPU-heavy Fibonacci computation
  3. Pass a Handle to a std thread and use it to run async DNS resolution
  4. Demonstrate the deadlock when calling block_on inside an async task
  5. Configure the blocking thread pool size with max_blocking_threads and observe behavior under load