Deep Dive into Safe Concurrency with Rust

Concurrency is a fundamental challenge in systems programming. Rust takes a radically different approach from other languages: rather than letting the developer manually manage risks, the compiler enforces strict rules that guarantee the safety of concurrent code at compile time. This article explores the Send and Sync traits, the pillars of this approach, through a concrete test project.

The Challenge: Two Functions in Parallel, Zero Bugs

The starting point of this exploration was a seemingly simple challenge: create a system where two functions can be called in parallel, without risking concurrency bugs (data races, deadlocks, memory corruption).

In Go, with its goroutines and channels, this kind of pattern is straightforward. In JavaScript, the single-threaded event loop avoids the problem by design (at the cost of true parallelism). In Python, the Global Interpreter Lock (GIL) prevents any real concurrency on CPU threads, which severely limits both precision and performance.

Rust makes this challenge more complex by imposing strict rules, but this is precisely what guarantees that the code is safe and performant, even in complex scenarios.

Send and Sync: The Guardians of Concurrency

At the heart of Rust's idiomatic approach to concurrent programming, we find two fundamental traits:

The Send Trait

A type that implements Send can be transferred from one thread to another safely. This means that ownership of the value can move from one thread to another without risk.

Most Rust types are Send. Notable exceptions are types that contain raw pointers or handles to thread-specific resources (like Rc<T>, the non-atomic reference counter).

The Sync Trait

A type that implements Sync can be shared between multiple threads via immutable references (&T). In other words, if T is Sync, then &T is Send.

Types that are not Sync include those whose internal state can be modified via a shared reference without synchronization (like Cell<T> or RefCell<T>).

The Fundamental Rule

These two traits are automatically derived by the compiler for composite types, provided that all their fields implement them. This means the compiler refuses to compile code that would attempt to send a non-Send type to another thread or share a non-Sync type between threads.

Practical Implementation with crossbeam

For this test project (available on GitHub), I used the crossbeam crate, which offers features reminiscent of Go-style programming: scoped threads, typed channels, and high-performance synchronization primitives.

crossbeam::scope is particularly interesting because it guarantees that all threads spawned within a scope complete before the scope exits, which allows using references to stack data without the risk of dangling references.

use crossbeam::scope;

let data = vec![1, 2, 3, 4];

scope(|s| {
    s.spawn(|_| {
        // We can safely use &data here
        println!("Thread 1: {:?}", &data);
    });
    s.spawn(|_| {
        println!("Thread 2: {:?}", &data);
    });
}).unwrap();

Why Not Bypass with unsafe?

One might be tempted to use non-Send or non-Sync types with unsafe blocks to force things through. But this goes against Rust's safety philosophy. The type system exists to prevent concurrency bugs, and bypassing it means losing the language's primary guarantee.

If a type is not Send or Sync, it is generally for a good reason: its internal implementation is not thread-safe. Forcing its concurrent use with unsafe reintroduces exactly the categories of bugs that Rust aims to eliminate.

The Result: Reliability and Complexity

The result of this exploration is reliable and performant code. Rust forces the developer to think about concurrency rigorously, and the compiler rejects any code that does not respect safety invariants.

This does come with increased complexity compared to more permissive languages. The learning curve is real, and some concurrent data structures require deep thought to satisfy the type system's constraints.

Comparison with Other Approaches

LanguageApproachCompile-time GuaranteesReal Parallelism
RustSend/Sync + ownershipYesYes
GoGoroutines + channelsNo (data races possible)Yes
JavaScriptSingle-threaded event loopN/ANo (except Web Workers)
PythonGILNoNo (CPU threads)
C/C++Manual mutexesNoYes

Rust is the only language in this list that detects concurrency errors at compile time, which is a considerable advantage for critical systems.

Conclusion

The Send and Sync traits are at the heart of what makes Rust unique for concurrent programming. They transform entire categories of runtime bugs into compilation errors, offering a guarantee that few other languages can match. The additional complexity they impose is the price to pay for uncompromising reliability, and it is a trade-off well worth making for robust and scalable systems.