Synchronization

When data is shared between threads, one needs to synchronize read-write access to the data in order to avoid corruption. The C# offers the lock keyword as a synchronization primitive (which desugars to exception-safe use of Monitor from .NET):

using System;
using System.Threading;

var dataLock = new object();
var data = 0;
var threads = new List<Thread>();

for (var i = 0; i < 10; i++)
{
    var thread = new Thread(() =>
    {
        for (var j = 0; j < 1000; j++)
        {
            lock (dataLock)
                data++;
        }
    });
    threads.Add(thread);
    thread.Start();
}

foreach (var thread in threads)
    thread.Join();

Console.WriteLine(data);

In Rust, one must make explicit use of concurrency structures like Mutex:

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

fn main() {
    let data = Arc::new(Mutex::new(0)); // (1)

    let mut threads = vec![];
    for _ in 0..10 {
        let data = Arc::clone(&data); // (2)
        let thread = thread::spawn(move || { // (3)
            for _ in 0..1000 {
                let mut data = data.lock().unwrap();
                *data += 1; // (4)
            }
        });
        threads.push(thread);
    }

    for thread in threads {
        thread.join().unwrap();
    }

    println!("{}", data.lock().unwrap());
}

A few things to note:

  • Since the ownership of the Mutex instance and in turn the data it guards will be shared by multiple threads, it is wrapped in an Arc (1). Arc provides atomic reference counting, which increments each time it is cloned (2) and decrements each time it is dropped. When the count reaches zero, the mutex and in turn the data it guards are dropped. This is discussed in more detail in Memory Management).

  • The closure instance for each thread receives ownership (3) of the cloned reference (2).

  • The pointer-like code that is *data += 1 (4), is not some unsafe pointer access even if it looks like it. It's updating the data wrapped in the mutex guard.

Unlike the C# version, where one can render it thread-unsafe by commenting out the lock statement, the Rust version will refuse to compile if it's changed in any way (e.g. by commenting out parts) that renders it thread-unsafe. This demonstrates that writing thread-safe code is the developer's responsibility in C# and .NET by careful use of synchronized structures whereas in Rust, one can rely on the compiler.

The compiler is able to help because data structures in Rust are marked by special traits (see Interfaces): Sync and Send. Sync indicates that references to a type's instances are safe to share between threads. Send indicates it's safe to instances of a type across thread boundaries. For more information, see the “Fearless Concurrency” chapter of the Rust book.