Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Thread-safe mutable non-owning pointer in Rust?

I'm trying to parallelize an algorithm I have. This is a sketch of how I would write it in C++:

void thread_func(std::vector<int>& results, int threadid) {
   results[threadid] = threadid;
}

std::vector<int> foo() {
  std::vector<int> results(4);

  for(int i = 0; i < 4; i++)
  {
     spawn_thread(thread_func, results, i);
  }

  join_threads();

  return results;
}

The point here is that each thread has a reference to a shared, mutable object that it does not own. It seems like this is difficult to do in Rust. Should I try to cobble it together in terms of (and I'm guessing here) Mutex, Cell and &mut, or is there a better pattern I should follow?

like image 376
anjruu Avatar asked Aug 27 '15 12:08

anjruu


People also ask

Does Rust guarantee thread safety?

Rust guarantees thread safety using similar concepts for memory safety and provides standard library features like channels, mutex, and ARC (Atomically Reference Counted) pointers. In safe Rust, you can have either one mutable reference to a value or unlimited read-only references to it at any given time.

Are pointers thread-safe?

Yes, the control block is thread-safe; but no, the access to the resource is not thread-safe. That means, modifying the reference counter is an atomic operation and you have the guarantee that the resource will be deleted exactly once. These are all guarantees a std::shared_ptr gives you.

Is RC thread-safe Rust?

The raw pointer types are not Send because Rust cannot make any guarantees about the synchronization mechanisms that are in the underlying types, or when the pointers will be accessed. Rc is not Send because it uses a non-atomic reference counter which is not safe to send between threads.

Is Rust good for multithreading?

Rust attempts to mitigate the negative effects of using threads, but programming in a multithreaded context still takes careful thought and requires a code structure that is different from that in programs running in a single thread.


1 Answers

The proper way is to use Arc<Mutex<...>> or, for example, Arc<RWLock<...>>. Arc is a shared ownership-based concurrency-safe pointer to immutable data, and Mutex/RWLock introduce synchronized internal mutability. Your code then would look like this:

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

fn thread_func(results: Arc<Mutex<Vec<i32>>>, thread_id: i32) {
    let mut results = results.lock().unwrap();
    results[thread_id as usize] = thread_id;
}

fn foo() -> Arc<Mutex<Vec<i32>>> {
    let results = Arc::new(Mutex::new(vec![0; 4]));

    let guards: Vec<_> = (0..4).map(|i| {
        let results = results.clone();
        thread::spawn(move || thread_func(results, i))
    }).collect();

    for guard in guards {
        guard.join();
    }

    results
}

This unfortunately requires you to return Arc<Mutex<Vec<i32>>> from the function because there is no way to "unwrap" the value. An alternative is to clone the vector before returning.

However, using a crate like scoped_threadpool (whose approach could only be recently made sound; something like it will probably make into the standard library instead of the now deprecated thread::scoped() function, which is unsafe) it can be done in a much nicer way:

extern crate scoped_threadpool;

use scoped_threadpool::Pool;

fn thread_func(result: &mut i32, thread_id: i32) {
    *result = thread_id;
}

fn foo() -> Vec<i32> {
    let results = vec![0; 4];
    let mut pool = Pool::new(4);

    pool.scoped(|scope| {
        for (i, e) in results.iter_mut().enumerate() {
            scope.execute(move || thread_func(e, i as i32));
        }
    });

    results
}

If your thread_func needs to access the whole vector, however, you can't get away without synchronization, so you would need a Mutex, and you would still get the unwrapping problem:

extern crate scoped_threadpool;

use std::sync::Mutex;

use scoped_threadpool::Pool;

fn thread_func(results: &Mutex<Vec<u32>>, thread_id: i32) {
    let mut results = results.lock().unwrap();
    result[thread_id as usize] = thread_id;
}

fn foo() -> Vec<i32> {
    let results = Mutex::new(vec![0; 4]);
    let mut pool = Pool::new(4);

    pool.scoped(|scope| {
        for i in 0..4 {
            scope.execute(move || thread_func(&results, i));
        }
    });

    results.lock().unwrap().clone()
}

But at least you don't need any Arcs here. Also execute() method is unsafe if you use stable compiler because it does not have a corresponding fix to make it safe. It is safe on all compiler versions greater than 1.4.0, according to its build script.

like image 68
Vladimir Matveev Avatar answered Oct 01 '22 17:10

Vladimir Matveev