1

It's well known that async code should not run for long between awaits. Nonetheless, sometimes async code needs to perform long-running operations, like blocking IO. In those cases, it seems the usual recommendation is to use tokio::task::spawn_blocking (for example, see this blog post from one of the Tokio developers).

The trouble is, as with regular tokio::task::spawn, spawn_blocking must own all the data it accesses (or it must have 'static lifetime), even when immediately awaiting it (see Why does `tokio::spawn` requires a `'static` lifetime if I immediately await it?).

As a result, code like this will not compile:

fn foo(my_ref: &ExpensiveToClone) {
    // Do some stuff that blocks for IO
}

async fn foo_the_reference(my_ref: &ExpensiveToClone) {
    spawn_blocking(move || {
        foo(my_ref);
    }).await;
}

Is there a way to run blocking code from async code without this static lifetime restriction?

Dominick Pastore
  • 4,177
  • 2
  • 17
  • 29

1 Answers1

2

If using tokio, you can use tokio::task::block_in_place. It still blocks the thread, but it at least notifies the executor that it will be blocked and should offload its tasks to other threads.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • Note that it is more expensive than `spawn_blocking()`, so you should avoid it unless you have a good reason. – Chayim Friedman Jul 15 '23 at 19:01
  • "It still blocks the thread" - Can you elaborate more on the implications of this in your answer? – Dominick Pastore Jul 15 '23 at 19:41
  • Perhaps @ChayimFriedman can corroborate since its not exactly spelled out in the tokio docs what the exact implications are, but: the way tokio works is to have a set of threads that execute the async tasks; using `spawn_blocking` uses totally different threads while `block_in_place` will "steal" one of the threads dedicated to asyncs tasks temporarily. The broader implications (if I'm correct) that if you do this too much you could end up starving your async tasks. – kmdreko Jul 15 '23 at 19:57
  • After further reading, it sounds like the gotcha is that there are various ways to execute futures concurrently within a single task, e.g. `join!`, `select!`, `StreamExt::for_each_concurrent`, etc., and using `block_in_place` will block the entire task, not just the async function/block where it resides. For example, `join!` polls each of its "child" futures in turn. If one blocks, `join!` is stuck waiting for it to unblock before it can poll the next future. However, anything that has been `spawn`ed can truly run in parallel on a different thread. – Dominick Pastore Jul 15 '23 at 23:01
  • That is very true as well and something to consider – kmdreko Jul 15 '23 at 23:05