While the push doesn't change the 1st element, unsafe code doesn't compile

use std::{thread::scope, net::TcpStream, io::Read};
fn main(){
    let mut a = TcpStream::connect("[::1]:1024").unwrap();
    let mut b = TcpStream::connect("[::1]:1025").unwrap();
    let mut v = vec![a];
    scope(|s|{
        s.spawn(||unsafe{v.push(b)});
        s.spawn(||{
            let mut r = [0u8; 8];
            unsafe{v[0].read(&mut r);}
        });
    })
}

The push doesn't change the 1st element, even unsafe block is denied when compiling. Is it possible to do those jobs simultaneously?

Two things:

  1. Unsafe does not turn off the usual rules. Code that uses no unsafe operations does not get extra capabilities just because you wrap them in unsafe.
  2. Your code is broken. The call to push will reallocate the vector, which involves moving the first element to a new allocation.
4 Likes

This is not true. push may (actually in this case it will!) reallocate, invalidating the previous storage, included the first element. Consider this shenario:

  • thread 2 acquires a reference to v[0]
  • thread 1 pushes, reallocating. The reference to v[0] becomes invalid
  • thread 2 actually uses the reference to v[0] to call read, but is now invalid and thus you get UB.

unsafe code doesn't let you directly bypass the borrow checker. It only allows you to (source):

  • Dereference a raw pointer
  • Call an unsafe function or method
  • Access or modify a mutable static variable
  • Implement an unsafe trait
  • Access fields of unions

You'll have to preallocate the memory for b to avoid any reallocations in v. Note that this is not fully general: depending on what you may want to actually do this could be unfeasible. For example Rust Playground

4 Likes

What if the final length is only known at runtime? E.g., the user specifies the length.

You don't have to know the length at compile-time. Methods like Vec::with_capacity work fine with numbers computed at runtime.

One correct solution to write to and read from a Vec concurrently is to use Arc and Mutex. For example:

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

fn main(){
    let v = vec![10];
    let v1 = Arc::new(Mutex::new(v));
    let v2 = v1.clone();
    scope(|s|{
        s.spawn(|| v1.lock().unwrap().push(20));
        s.spawn(|| {
            let x = v2.lock().unwrap()[0];
            println!("Got: {x:?}");
        });
    })
}

(Playground)

Output:

Got: 10

2 Likes

The Arc is not necessary there due to the use of scoped threads.

3 Likes

Yes, thanks for pointing that out. I just noticed myself. I haven't used scoped threads yet.

I added a final assertion to demonstrate that the two threads are really executed:

use std::thread::scope;
use std::sync::Mutex;

fn main(){
    let v = vec![10];
    let m = Mutex::new(v);
    scope(|s|{
        s.spawn(|| m.lock().unwrap().push(20));
        s.spawn(|| {
            let x = m.lock().unwrap()[0];
            println!("Got: {x:?}");
        });
    });
    let v = m.into_inner().unwrap();
    assert_eq!(v, vec![10, 20]);
}

(Playground)

1 Like

If I use std::thread::spawn, compiler will say std::sync::MutexGuard<'_, std::vec::Vec<std::net::TcpStream>> cannot be sent between threads safely.

If you use std::thread::spawn, you also need the Arc. The following example compiles on Playground:

use std::{thread::spawn, net::TcpStream, io::Read};
use std::sync::{Arc, Mutex};

fn main() {
    let a = TcpStream::connect("[::1]:1024").unwrap();
    let b = TcpStream::connect("[::1]:1025").unwrap();
    let v = vec![a];
    let v1 = Arc::new(Mutex::new(v));
    let v2 = v1.clone();
    let t1 = spawn(move || v1.lock().unwrap().push(b));
    let t2 = spawn(move || {
        let mut r = [0u8; 8];
        v2.lock().unwrap()[0].read(&mut r).unwrap();
    });
    t1.join().unwrap();
    t2.join().unwrap();
}

(Playground)

Note that I added the keyword move in front of the closures to be run in threads.

1 Like

I wrote .clone().lock().unwrap() within 1 line, it didn't compile. I have to write .clone() and .lock().unwrap() separatedly.

Yes, the way an Arc lets you share stuff is via the .clone() method. Cloning an Arc gives you a new "handle" to the same shared value — the value inside it is not cloned. Now, any single clone of the Arc can only be in one thread, so the way you share them is by first making a clone, and then giving the clone to the new thread. (You can't call clone inside the new task! It has to happen before you spawn.)

3 Likes

It seems that Rust Playground doesn't support IPv6, TcpListener can't bind to [::1]:1025 .