Concurrency and Mutex lock with Thread spawns


#1

Questions

I prepared the Code Snippet at the bottom of this post whilst applying principles from the Concurrency chapter of the official Rust guide book. My questions are:

  • Why is it that sometimes when I run the Code Snippet the output is as shown in (A) below, whilst at other times the output is as shown in (B) below?:

(A) output

Data immediately after thread mutation: Mutex { <locked> }
Data 5ms after thread mutation: Mutex { data: [2, 3, 3] }
Data 300ms after thread mutation: Mutex { data: [2, 3, 3] }

(B) output

Data immediately after thread mutation: Mutex { data: [1, 2, 3] }
Data 5ms after thread mutation: Mutex { data: [2, 3, 3] }
Data 50ms after thread mutation: Mutex { data: [2, 3, 3] }
  • Why doesn’t the last element of the Vector get incremented (even if I apply a longer delay before the final println!). The output never appears to become Mutex { data: [2, 3, 4] }

  • It is my understanding that Thread::spawn returns a “detached” New Thread directly, whereas using Thread::scoped returns a Join Guard from which the Thread may be extracted.
    Does an RAII-style Join Guard (or any form of Guard at all) get returned when using Thread::spawn that will block until the New Thread is terminated? Or are Guards not relevant when just using Thread::spawn?

  • What does the statement “perform low-level synchronization” mean in the context of each New Thread that is returned when using Thread::spawn? How does this work and what does it achieve?

My Understanding of How the Code Snippet works
In the following Concurrency code snippet Arc pointer type has been used so that the “Send” Trait may be implemented in order to allow a “guard” to be transferred across Thread boundaries (when using “scoped” Threads), and to check with the Rust ownership system that its contents implement the “Sync” Trait (“Atomic” and safe to share across multiple threads).
The clone() method is called upon the original Vector to perform Arc (Automatic Reference Counted) and increase the internal count for memory stack allocation and deallocation bookkeeping.
The Mutex type ensures only one person may mutate the value of the of its Vector contents at a time, and has a lock() method that must be called to try and open the lock, and upon success the unwrap() method may be called to retrieve and mutate its data contents Vector.
In the block where the New Thread is spawned (“detached” from the Current Thread that it may outlive) we call the lock() method, acquire the Mutex lock successfully, and call unwrap() to retrieve a reference to the Vector data for subsequent mutation through incrementing the respective Vector element’s value.

The Code Snippet itself

// Import Types including Atomic Reference Counted Pointer and Mutex
use std::sync::{Arc, Mutex};

// Import Threads from Rust Standard Library to allow running code in parallel
use std::thread;

// Import Sleep methods
use std::old_io::timer;
use std::time::Duration;

fn main() {
    try_concurrency();
}

fn try_concurrency() {

    let data = Arc::new(Mutex::new(vec![1u32, 2, 3]));

    println!("Data before thread mutation: {:?}", data);

    for i in 0..2 {
        let data = data.clone();

        thread::spawn(move || {
            let mut data = data.lock().unwrap();
            data[i] += 1;
        });
    }

    println!("Data immediately after thread mutation: {:?}", data);

    timer::sleep(Duration::milliseconds(5));

    println!("Data 5ms after thread mutation: {:?}", data);

    timer::sleep(Duration::milliseconds(50));

    println!("Data 50ms after thread mutation: {:?}", data);

}

#2

The spawned threads are running concurrently with the main thread. The Debug implementation for Mutex will print <locked> if the mutex is already locked (to try to avoid causing deadlocks by locking the mutex in a “hidden” way). It may so happen that the main thread reaches the println! before (or after) the spawned threads, in which case the Mutex can be locked by the main thread and so the data printed. However it is also possible that the spawned threads have locked the mutex by the time the main thread gets to the println!, in which case the main thread won’t relock it, and will just print <locked> instead.

This is the problem:

0..2 is like range(0, 2) in Python, or for (int i = 0; i < 2; i++) in C: the right end-point is not included. Change to 0..3 to work properly.

The docs have some detail: http://doc.rust-lang.org/nightly/std/thread/fn.spawn.html

If that’s confusing/unclear feel free to ask questions (and maybe we can improve those docs).

I assume it is referring to the ability to join, and the park/unpark abstraction.


#3

spawn creates a new thread that executes concurrently with the main thread. When you try to get lock the mutex immediately after spawning, sometimes the other running thread is in the middle of holding the lock, others not.

The second bound in range syntax is exclusive, so 0..2 iteraters over 0 and 1 only.

No, the JoinHandle returned by spawn does not block on destruction, but you can block on it by calling .join().

I’m not sure where this quote is from.

You are using an Arc to share the Mutex value, which implements Sync, indicating that it can be shared between threads. There is no scoped thread in your example.

Yes, clone bumps the atomic ref count so that multiple pointers to the same Mutex can be shared between your threads. The ref count tracks the heap allocation that holds the Mutex, not the stack memory, though clone produces another copy of the Arc, which itself lives on the stack (containing a pointer to the Mutex on the heap).

Yes.

Yes.