I currently have a function that takes a Writer and writes into it. I need to change it to make the write async. Here's a simplified version of my attempt:
link: Rust Playground
code:
use std::io::Write;
use std::thread;
fn main() {
let mut buffer = Vec::new();
write_to(&mut buffer);
}
fn write_to<W: Write>(mut buffer: W) {
thread::spawn(move||{
buffer.write(b"hello").unwrap();
});
}
This, of course, doesn't compile because Write cannot be shared between threads. My main question is, why? And how to solve it?
The write_to function is supposed to be exposed to users of my library. They call it passing a writer but never actually read from it. It could be a file or stdout. For now I wouldn't like to use channels for this, unless it is only internally.
This is a continuation from my previous question here: How to test output to stdout
1 Like
What needs to be sent (in this case, not shared) between threads is not Write
(that's only a trait), but W
. You can give a W: Write + Send + 'static
bound. (The 'static
bound is required because it makes sure that W
doesn't contain anything that might not live as long as the thread.)
You'll still have a problem because you're passing a mutable reference to write_to
; this will run into a lifetime error because the thread can outlive main()
. Passing write_to(buffer)
will work.
<anon>:10:5: 10:18 error: the trait `core::marker::Send` is not implemented for the type `W` [E0277]
<anon>:10 thread::spawn(move||{
^~~~~~~~~~~~~
fn write_to<W: Write + Send>(mut buffer: W) {
...
}
<anon>:10:5: 10:18 error: the parameter type `W` may not live long enough [E0310]
<anon>:10 thread::spawn(move||{
^~~~~~~~~~~~~
<anon>:10:5: 10:18 help: see the detailed explanation for E0310
<anon>:10:5: 10:18 help: consider adding an explicit lifetime bound `W: 'static`...
fn write_to<W: Write + Send + 'static>(mut buffer: W) {
...
}
<anon>:6:19: 6:25 error: `buffer` does not live long enough
<anon>:6 write_to(&mut buffer);
^~~~~~
note: reference must be valid for the static lifetime...
fn main() {
let mut buffer = Vec::new();
write_to(buffer);
}
<anon>:5:9: 5:19 warning: variable does not need to be mutable, #[warn(unused_mut)] on by default
<anon>:5 let mut buffer = Vec::new();
^~~~~~~~~~
fn main() {
let buffer = Vec::new();
write_to(buffer);
}
Which only really makes sense if you're writing to a file, as otherwise the changes just disappear. If you want to write to something that you then read back from, well, that's a different kettle of fish; you'll need to either have the thread pass ownership of the W
back (which means blocking, which means it'd be easier to not bother with a thread in the first place or just use a channel and return that), or writing an implementation of Write
for something like Arc<Mutex<W>>
which is pretty straightforward.
1 Like
How does that even work, if Vec doesn't implement Send?
Well, it does. Send
is one of the weird traits (known as OIBITs; don't worry about what it means, the name is deeply misleading and will probably change) that is automatically implemented on everything, except for types where it is specifically un-implemented, or types containing other types for which it is un-implemented.
Practical upshot: Vec
is Send
, although there's no direct way to know that other than just trying it and seeing if it works. It makes sense because Vec
owns its data, and isn't tied to a particular thread, so there's no reason you shouldn't be able to send them between threads.
1 Like
Ha! Thanks for explanation!
"This trait is automatically derived when the compiler determines it's appropriate."
I must have missed that when reading the documentation.
I'll try to write a Write
implementation for Arc<Mutex<W>>
, see how much fun it is xD
I was able to sketch the general idea: Rust Playground
use std::io;
use std::io::Write;
use std::fmt::{Debug, Formatter, Error};
use std::thread;
use std::time::Duration;
use std::sync::{Arc, Mutex};
struct Output<W>(Arc<Mutex<W>>);
impl<W: Write> Write for Output<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(*self.0.lock().unwrap()).write(buf)
}
fn flush(&mut self) -> io::Result<()> {
(*self.0.lock().unwrap()).flush()
}
}
impl<W: Write> Clone for Output<W> {
fn clone(&self) -> Self {
Output(self.0.clone())
}
}
impl<W: Debug> Debug for Output<W> {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
(*self.0.lock().unwrap()).fmt(fmt)
}
}
fn main() {
let buffer = Output(Arc::new(Mutex::new(Vec::new())));
write_to(&buffer);
thread::sleep(Duration::new(1, 0));
println!("{:?}", buffer);
}
fn write_to<W: Write + Send + 'static>(buffer: &Output<W>) {
let mut thread_buffer = buffer.clone();
thread::spawn(move||{
thread_buffer.write(b"hello").unwrap();
});
}
Any advice?
It depends on the desired API if the Output
type is necessary. You could also simply do
fn write_to<W: Write + Send + 'static>(buffer: &Arc<Mutex<W>>) {
let thread_buffer = buffer.clone();
thread::spawn(move || {
thread_buffer.lock().unwrap().write(b"hello").unwrap();
});
}
Awesome!
But I think the API will benefit more of an abstraction around output.
You can simplify it further:
use std::io;
use std::io::Write;
use std::fmt::{Debug, Formatter, Error};
use std::thread;
use std::time::Duration;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Output<W>(Arc<Mutex<W>>);
impl<W: Write> Output<W> {
pub fn new(w: W) -> Self {
Output(Arc::new(Mutex::new(w)))
}
}
impl<W: Write> Write for Output<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(*self.0.lock().unwrap()).write(buf)
}
fn flush(&mut self) -> io::Result<()> {
(*self.0.lock().unwrap()).flush()
}
}
impl<W: Debug> Debug for Output<W> {
fn fmt(&self, fmt: &mut Formatter) -> Result<(), Error> {
(*self.0.lock().unwrap()).fmt(fmt)
}
}
fn main() {
let buffer = Output::new(Vec::new());
write_to(buffer.clone());
thread::sleep(Duration::new(1, 0));
println!("{:?}", buffer);
}
fn write_to<W: Write + Send + 'static>(mut buffer: W) {
thread::spawn(move || buffer.write(b"hello").unwrap());
}
There's no need to specialise write_to
; the whole point of Output
is that it's something that is both Write
and Send
and shareable... but write_to
only needs those first two things. Also, if you are always going to clone, then it's better to just take by-value and let the caller decide if they need to clone or not.
5 Likes