Future is not Send inside tokio::spawn

I've got stuck with the following compiler error:

error: future cannot be sent between threads safely
   --> src/lua_test.rs:7:5
    |
7   |     tokio::spawn(async {
    |     ^^^^^^^^^^^^ future created by async block is not `Send`
    | 
   ::: /home/pkolaczk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.11.0/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ---- required by this bound in `tokio::spawn`
    |
    = help: within `Lua`, the trait `Sync` is not implemented for `*mut c_void`
note: future is not `Send` as this value is used across an await
   --> src/lua_test.rs:10:9
    |
9   |         let function: Function = lua.globals().get("function").unwrap();
    |             -------- has type `LuaFunction<'_>` which is not `Send`
10  |         foo().await;
    |         ^^^^^^^^^^^ await occurs here, with `function` maybe used later
11  |         function.call(());
12  |     });
    |     - `function` is later dropped here

The code (minimized example):

use mlua::{Lua, Function};

async fn foo() {
}

async fn test_lua() {
    tokio::spawn(async {
        let lua = Lua::new();
        let function: Function = lua.globals().get("function").unwrap();
        foo().await;
        function.call::<(), ()>(());
    });
}

The problem happens because function holds a reference to lua and Lua is not Sync.
According to the docs, Lua is Send, but not Sync.

If I remove the await, it compiles fine.
If I move the await above the function initialization, it also compiles fine.

Can someone explain to me, why the compiler insists Lua must be Sync in this case?
Is there any way out?

I actually want to run my tokio task on a single thread, but a different thread than the thread that spawns it. So spawn_local doesn't really solve it. Assuming I won't be able to magically make Lua Sync, I need something that could run a non-Send Future on a background thread (but I don't want to move anything between threads here).

lol, I've spent 2 hours trying to solve this first, and just after minimizing the problem to post it on this forum, I quickly found the solution. Rubber-duck effect. :smiley:

Posting it so anyone who has the same problem won't waste so much time:

async fn test_lua() {
    std::thread::spawn(|| {
        tokio::task::spawn_local(async {
            let lua = Lua::new();
            let function: Function = lua.globals().get("function").unwrap();
            foo().await;
            function.call::<(), ()>(());
        });
    });
}

BTW: This is still open. Any explanations why the first doesn't work and the second one does work are welcome. I still feel a bit unsure about how Send/Sync relates to await and spawning, and essentially why it thought there was any sharing in the first code snippet.

BTW 2: The solution is not perfect though, because it launches a new thread. What if I insisted to run the task on one of the tokio worker threads with tokio::spawn?

1 Like

It's true that the Lua object is Send, but things like Function are not.

You wont be able to use them inside a normal spawned task because those could be moved across threads at any .await.

Your spawn_local thing might compile, but it will panic at runtime as spawn_local can only be used inside a LocalSet.

1 Like

One thing to understand about async/await in rust is that compiles to a state machine that captures all of the variables involved in the code block. That state machine can then be moved between threads at await points as @alice points out. Since the state machine is custom for each async code block, it is intelligent enough to know exactly which captured variables are used across awaits and therefore must be Send since they might move between threads (which Function is not as stated by @alice).

My recommendation would be to write a wrapper struct around your Lua object, then define non-async methods on that struct. You should then perform all of your use of Lua inside those non-async methods. You can call those non-async methods from async code if you wish.

If you want the container struct to be Sync, then wrap the Lua object inside it in a std::sync::Mutex and lock it to access Lua.

2 Likes

LocalSet looks like the solution I exactly need and my use-case is even described there.
I really don't want stuff to be moved between threads on await boundaries.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.