10

I am new to Rust, and struggling to deal with all those wrapper types in Rust. I am trying to write code that is semantically equal to the following C code. The code tries to create a big table for book keeping, but will divide the big table so that every thread will only access their local small slices of that table. The big table will not be accessed unless other threads quit and no longer access their own slice.

#include <stdio.h>
#include <pthread.h>

void* write_slice(void* arg) {
    int* slice = (int*) arg;
    int i;
    for (i = 0; i < 10; i++)
        slice[i] = i;
        
    return NULL;
}

int main()
{
    int* table = (int*) malloc(100 * sizeof(int));
    int* slice[10];
    int i;
    for (i = 0; i < 10; i++) {
      slice[i] = table + i * 10;
    }
    
    // create pthread for each slice
    pthread_t p[10];
    for (i = 0; i < 10; i++)
        pthread_create(&p[i], NULL, write_slice, slice[i]);
    
    for (i = 0; i < 10; i++)
        pthread_join(p[i], NULL);
        
    for (i = 0; i < 100; i++)
        printf("%d,", table[i]);
}

How do I use Rust's types and ownership to achieve this?

vallentin
  • 23,478
  • 6
  • 59
  • 81
qinsoon
  • 1,433
  • 2
  • 15
  • 34
  • 5
    Where is the Rust code that doesn't work, links to documentation you don't understand, or any research whatsoever that shows that you tried to solve this problem on your own? Stack Overflow isn't intended as a "write my code for me" service. – Shepmaster Nov 20 '15 at 04:06
  • Possible duplicate of http://stackoverflow.com/questions/32750829/passing-a-reference-to-a-stack-variable-to-a-scoped-thread. – Shepmaster Nov 20 '15 at 04:08

1 Answers1

19

Let's start with the code:

// cargo-deps: crossbeam="0.7.3"
extern crate crossbeam;

const CHUNKS: usize = 10;
const CHUNK_SIZE: usize = 10;

fn main() {
    let mut table = [0; CHUNKS * CHUNK_SIZE];

    // Scoped threads allow the compiler to prove that no threads will outlive
    // table (which would be bad).
    let _ = crossbeam::scope(|scope| {
        // Chop `table` into disjoint sub-slices.
        for slice in table.chunks_mut(CHUNK_SIZE) {
            // Spawn a thread operating on that subslice.
            scope.spawn(move |_| write_slice(slice));
        }
        // `crossbeam::scope` ensures that *all* spawned threads join before
        // returning control back from this closure.
    });

    // At this point, all threads have joined, and we have exclusive access to
    // `table` again.  Huzzah for 100% safe multi-threaded stack mutation!
    println!("{:?}", &table[..]);
}

fn write_slice(slice: &mut [i32]) {
    for (i, e) in slice.iter_mut().enumerate() {
        *e = i as i32;
    }
}

One thing to note is that this needs the crossbeam crate. Rust used to have a similar "scoped" construct, but a soundness hole was found right before 1.0, so it was deprecated with no time to replace it. crossbeam is basically the replacement.

What Rust lets you do here is express the idea that, whatever the code does, none of the threads created within the call to crossbeam::scoped will survive that scope. As such, anything borrowed from outside that scope will live longer than the threads. Thus, the threads can freely access those borrows without having to worry about things like, say, a thread outliving the stack frame that table is defined by and scribbling over the stack.

So this should do more or less the same thing as the C code, though without that nagging worry that you might have missed something. :)

Finally, here's the same thing using scoped_threadpool instead. The only real practical difference is that this allows us to control how many threads are used.

// cargo-deps: scoped_threadpool="0.1.6"
extern crate scoped_threadpool;

const CHUNKS: usize = 10;
const CHUNK_SIZE: usize = 10;

fn main() {
    let mut table = [0; CHUNKS * CHUNK_SIZE];

    let mut pool = scoped_threadpool::Pool::new(CHUNKS as u32);

    pool.scoped(|scope| {
        for slice in table.chunks_mut(CHUNK_SIZE) {
            scope.execute(move || write_slice(slice));
        }
    });

    println!("{:?}", &table[..]);
}

fn write_slice(slice: &mut [i32]) {
    for (i, e) in slice.iter_mut().enumerate() {
        *e = i as i32;
    }
}
smbear
  • 1,007
  • 9
  • 17
DK.
  • 55,277
  • 5
  • 189
  • 162
  • This brings up an interesting question: This solution didn't involve any atomic operators or guards. Does rust's threading model provide that all of the side effects of a thread are visible to other threads after that thread has joined? Is there a case where the worker threads could finish, but the main thread still sees the unchanged memory? – Lucretiel Nov 22 '18 at 00:16
  • 1
    @Lucretiel: I've never seen an explicit guarantee written down, but threads would be horrifically broken if they didn't. I'm reasonably sure that synchronising the parent and child threads as the child threads terminate should be enough: they both need to agree that the child thread has finished, and those writes take place before said synchronisation. – DK. Nov 22 '18 at 00:50
  • Not necessarily; a lot of modern thread doctrine calls for the use of atomic data structures, like lockless queues, to communicate between threads. I believe that `std::thread` uses such a mechanism to pass return values from threads back to the `thread.join()` – Lucretiel Nov 22 '18 at 00:51
  • 3
    @Lucretiel: The main thread observing the "I'm done" signal from the child thread implies it can also see the other writes it did *prior* to that signal. It shouldn't matter if there's an explicit lock or not; what matters is that the two agree on what point in time they can both see. If you're not convinced, you should open a new question: I'm not an expert, and comments aren't an appropriate place for this. – DK. Nov 22 '18 at 00:55