Synchronisation in long running computations in tokio

⚓ Rust    📅 2026-04-01    👤 surdeus    👁️ 5      

surdeus

I'd like to run a very expensive computational routine on global app state which performs some async IO on a multithreaded tokio runtime from within an axum handler.

Is the following a correct way of doing this?

type GlobalState = Arc<std::sync::RwLock<App>>;

pub async fn handler(State(app_state): State<GlobalState>) {
    let mut lock_guard = app_state.write().unwrap();

    tokio::task::block_in_place(move || Handle::current().block_on(lock_guard.long_running_future()));
}

The docs state:

Runs the provided blocking function on the current thread without blocking the executor.

In general, issuing a blocking call or performing a lot of compute in a future without yielding is problematic, as it may prevent the executor from driving other tasks forward. Calling this function informs the executor that the currently executing task is about to block the thread, so the executor is able to hand off any other tasks it has to a new worker thread before that happens.

Runs a future to completion on this Handle’s associated Runtime.

This runs the given future on the current thread, blocking until it is complete, and yielding its resolved result.

I have a tough time understanding the documentation here. My interpretation is that the block_in_place signals to the currently active runtime, that the worker thread is about to become uncooperative (long time to the next yield). Once I've notified the runtime I should be able to block the thread by running my very long computation.

Computation is async because it saves data to the DB and I assumed that explicitly blocking on these within the function to be unnecessary if I just block on the whole async function instead.

Essentially the future should run uninterrupted within the same thread is my understanding. All awaits within the routine will be blocking so I shouldn't have to worry about my lock not being async aware, as the future holding the lock will not be moved between other worker threads.

Besides my general lack of confidence that this approach is sound, there's the issue of cancellation. Docs state:

Code running behind block_in_place cannot be cancelled. When you shut down the executor, it will wait indefinitely for all blocking operations to finish. You can use shutdown_timeout to stop waiting for them after a certain timeout. Be aware that this will still not cancel the tasks — they are simply allowed to keep running after the method returns.

I wouldn't think that cancellation matters, since I'm not yielding to the executor until I'm done anyways. As to executor shutdown, that also is not something I should worry about as executor shutdown would occur only on application shutdown (?).

Having said all of the above the problem is that I know for a fact that this deadlocks (future never resolves, lock is never released, and application hangs) under certain circumstances - not too sure why, but it happens when I close the TCP connection while the computations are running.

I know that message passing is the preferred way of doing task synchronisation in tokio, but I'd like to understand what went wrong here to better understand the runtime's behaviour.

1 post - 1 participant

Read full topic

🏷️ Rust_feed