How to use fmt inside multi-threaded futures?

Consider this:

It fails with:

error[E0277]: `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
  --> src/main.rs:12:89
   |
12 |       async fn write_fmt<'args>(&mut self, args: fmt::Arguments<'args>) -> io::Result<()> {
   |  _________________________________________________________________________________________^
13 | |         let string = format!("{}", args);
14 | |         self.write_all(string.as_bytes()).await
15 | |     }
   | |_____^ `*mut (dyn std::ops::Fn() + 'static)` cannot be shared between threads safely
   |
   = help: within `[std::fmt::ArgumentV1<'_>]`, the trait `std::marker::Sync` is not implemented for `*mut (dyn std::ops::Fn() + 'static)`
   = note: required because it appears within the type `std::marker::PhantomData<*mut (dyn std::ops::Fn() + 'static)>`
   = note: required because it appears within the type `core::fmt::Void`
   = note: required because it appears within the type `&core::fmt::Void`
   = note: required because it appears within the type `std::fmt::ArgumentV1<'_>`
   = note: required because it appears within the type `[std::fmt::ArgumentV1<'_>]`
   = note: required because of the requirements on the impl of `std::marker::Send` for `&[std::fmt::ArgumentV1<'_>]`
   = note: required because it appears within the type `std::fmt::Arguments<'_>`
   = note: required because it appears within the type `[static generator@src/main.rs:12:89: 15:6 _self:&mut tokio::io::stdout::Stdout, args:std::fmt::Arguments<'_> for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6> {&'r mut tokio::io::stdout::Stdout, std::fmt::Arguments<'s>, std::string::String, &'t0 mut tokio::io::stdout::Stdout, &'t1 mut tokio::io::stdout::Stdout, &'t2 std::string::String, std::string::String, &'t3 [u8], &'t4 [u8], tokio::io::util::write_all::WriteAll<'t5, tokio::io::stdout::Stdout>, tokio::io::util::write_all::WriteAll<'t6, tokio::io::stdout::Stdout>, ()}]`
   = note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:12:89: 15:6 _self:&mut tokio::io::stdout::Stdout, args:std::fmt::Arguments<'_> for<'r, 's, 't0, 't1, 't2, 't3, 't4, 't5, 't6> {&'r mut tokio::io::stdout::Stdout, std::fmt::Arguments<'s>, std::string::String, &'t0 mut tokio::io::stdout::Stdout, &'t1 mut tokio::io::stdout::Stdout, &'t2 std::string::String, std::string::String, &'t3 [u8], &'t4 [u8], tokio::io::util::write_all::WriteAll<'t5, tokio::io::stdout::Stdout>, tokio::io::util::write_all::WriteAll<'t6, tokio::io::stdout::Stdout>, ()}]>`
   = note: required because it appears within the type `impl std::future::Future`
   = note: required because it appears within the type `impl std::future::Future`
   = note: required for the cast to the object type `dyn std::future::Future<Output = std::result::Result<(), std::io::Error>> + std::marker::Send`

This is because fmt::Arguments are !Send and !Sync. This means they cannot even be put in a mutex. So how do I use them in multi-threaded futures? Note that without fmt::Arguments it is impossible to use fmt::Debug and fmt::Display.

What I would do is create a logger and an MPSC channel, make the logger be the consumer of the channel, and then have every async fn simply clone a producer endpoint of the channel when it's initialized.

That way, each async fn can request a log to take place by sending a mag to the logger, rather than logging the msg itself.

As a bonus it can actually turn out to be faster then direct logging because you can control when you flush stdio and/or stderr.

What do I send through the channel?

I cannot send the whole owned object, cause it would move the object away.

I cannot borrow the object, cause either 'static would be required or I would have to hang my future.

I cannot send a formatted string or format arguments, because invoking format! and format_args! is the whole source of errors.

Try dropping them before calling await.

Sorry, drop who? where?

this didn't work

You want the Arguments value to be not stored in the future, so it needs to be dropped before it reaches an await point. I suppose in this case the issue is that it exists during the await point that always implicitly exists at the start of the function, so you should probably switch to a non-async function that returns an impl Future and convert the arguments to a string, then move the string into an async block, which you can then return.

Since it's in a trait, you will need an Pin<Box<dyn Future>> instead of impl Future.

1 Like

Oh, this works! Rust Playground

What won't work is this: Rust Playground

I guess it is possible to make a wrapper over write!

just to finish: this is what ended up working for me!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.