Nick
Nick

Reputation: 10499

Why does my Fn need to be Sync when spawned by Tokio?

This is the code that raises a compiler error suggesting that MyFn should be restricted with a Sync bound:

use std::future::Future;

fn spawn<MyFn, Out>(func: MyFn)
where
    Out: Future + Send + 'static,
    MyFn: Fn() -> Out + Send + 'static,
{
    tokio::spawn(call_func(func));
}

async fn call_func<MyFn, Out>(func: MyFn)
where
    MyFn: Fn() -> Out,
    Out: Future,
{
    func().await;
    func().await;
}

Link to the Playground.

This raises the following error:

Compiling playground v0.0.1 (/playground)
error: future cannot be sent between threads safely
   --> src/main.rs:8:18
    |
8   |     tokio::spawn(call_func(func));
    |                  ^^^^^^^^^^^^^^^ future returned by `call_func` is not `Send`
    |
note: future is not `Send` as this value is used across an await
   --> src/main.rs:16:11
    |
16  |     func().await;
    |     ----  ^^^^^^ await occurs here, with `func` maybe used later
    |     |
    |     has type `&MyFn` which is not `Send`
note: `func` is later dropped here
   --> src/main.rs:16:17
    |
16  |     func().await;
    |                 ^
help: consider moving this into a `let` binding to create a shorter lived borrow
   --> src/main.rs:16:5
    |
16  |     func().await;
    |     ^^^^^^
note: required by a bound in `tokio::spawn`
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.2/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`
help: consider dereferencing here
    |
8   |     tokio::spawn(*call_func(func));
    |                  +
help: consider further restricting this bound
    |
6   |     MyFn: Fn() -> Out + Send + 'static + std::marker::Sync,
    |                                        +++++++++++++++++++

error: could not compile `playground` due to previous error

Why exaclty does MyFn need to be restricted using the Sync bound, when all that tokio::spawn requires is the following?

pub fn spawn<T>(future: T) -> JoinHandle<T::Output>ⓘ
where
    T: Future + Send + 'static,
    T::Output: Send + 'static,

Upvotes: 1

Views: 412

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43753

In this part of your code:

async fn call_func<MyFn, Out>(func: MyFn)
where
    MyFn: Fn() -> Out,
    Out: Future,
{
    func().await;
    func().await;
}

you are calling func twice. The way this is possible — why it doesn't violate Rust's move semantics even though MyFn does not have a Copy bound — is that func is being called by & reference, since it is a Fn and the definition of Fn is that you can call a function if you have & to it. Then, since there is an await in the middle, this code might move between threads. So, references to the value func is being used from two different threads, so &MyFn: Send is required, so MyFn: Sync is required.

Here's one trick to solve the problem: declare the function as FnMut instead of Fn. This means that a &mut reference is understood to be required to call the function, and since &mut references are unique they don't require Send of their referent. These changes compile successfully:

fn spawn<MyFn, Out>(func: MyFn)
where
    Out: Future + Send + 'static,
    MyFn: FnMut() -> Out + Send + 'static,
{
    tokio::spawn(call_func(func));
}

async fn call_func<MyFn, Out>(mut func: MyFn)
where
    MyFn: FnMut() -> Out,
    Out: Future,
{
    func().await;
    func().await;
}

In principle, the compiler could observe that the internal &MyFn never needs to move between threads, and do the equivalent of my FnMut rewrite internally (every Fn is also a FnMut), but evidently it doesn't.

Upvotes: 2

Related Questions