Shared callback in threads

Hi there,

I'd like to share a callback among multiple threads. So I wrap it in an Arc<Mutex<>> and send clone()s of it to the different threads.
Unfortunately I cannot find how to create an Arc<Mutex<>> closure correctly. The following fails with the error seen at the bottom. Any help is greatly appreciated.

use std::sync::{Arc, Mutex};

type CB = Arc<Mutex<Box<dyn FnMut()>>>;

fn use_cb(callback: CB){
    if let Ok(mut cb) = callback.try_lock(){
        cb();
    }
}

fn main(){
    let callback = Arc::new(Mutex::new(Box::new(|| println!("callback"))));
    use_cb(callback);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:13:12
   |
12 |     let callback = Arc::new(Mutex::new(Box::new(|| println!("callback"))));
   |                                                 ----------------------- the found closure
13 |     use_cb(callback);
   |            ^^^^^^^^ expected trait object `dyn FnMut`, found closure
   |
   = note: expected struct `Arc<Mutex<Box<(dyn FnMut() + 'static)>>>`
              found struct `Arc<Mutex<Box<[closure@src/main.rs:12:49: 12:72]>>>`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

The return value of Box::new(|| ...) is Box<SomeAnonymousCallbackType>, but you need an Box<dyn FnMut()>. This requires a conversion. The compiler will insert this conversion automatically sometimes, but in this case it needs your help to figure out where the conversion needs to go.

fn main(){
    let b: Box<dyn FnMut()> = Box::new(|| println!("callback"));
    let callback = Arc::new(Mutex::new(b));
    use_cb(callback);
}

Note that an alternative would be Arc<dyn Fn()>. This would remove the need for locks if you are ok with using Fn() instead of FnMut().

2 Likes

If you do need the FnMut in your real code, Arc<Mutex<dyn FnMut()>> is possible. It even works with your original code.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fa6115ccdbc07d6d7780c02edf9480cd

2 Likes

Ah, now I understand why some of the rust-wasm code as these as Box<dyn FnMut> when defining the callbacks...

Is there an easy explanation what the better solution is to share a callback in threads:

  1. Arc<Mutex<Box<dyn FnMut>>>
    or
  2. Arc<Mutex<dyn FnMut()>>

Now that I see the 2. is possible, I think I prefer that one, as it's shorter. Does it have any disadvantages?

I tried a lot of combinations, but I think I never got rid of the Box. So it's not needed here?

Thanks a lot!

The second one requires the mutex to be constructed before the closure is converted into dyn FnMut. In practice, this means that you won't be able to replace the closure inside the mutex with a different one later. With the Box, you can lock the mutex and swap in a new closure whenever you want:

// impossible with &Mutex<dyn FnMut()>
fn replace(mutex: &Mutex<Box<dyn FnMut()>>, f: impl FnMut() + 'static) {
    *mutex.lock().unwrap() = Box::new(f);
}
4 Likes

Great! That was the answer to the other question I didn't even ask yet :wink:

By the way, the minimal change needed to make the original code compile would be an additional “as _”, i.e.:

fn main() {
    let callback = Arc::new(Mutex::new(Box::new(|| println!("callback")) as _));
    use_cb(callback);
}

This explicitly tells the compiler where the coercion is supposed to happen and it then figures out the types involved for you by reasoning from both sides properly.

3 Likes