Image by Gunnar Pippel on Shutterstock

PART I

Fearless Concurrency with Rust

Safety

Herbert Wolverson
The Pragmatic Programmers
4 min readJun 7, 2022

--

https://pragprog.com/newsletter/
https://pragprog.com/newsletter/

One of Rust’s selling points is Fearless Concurrency. Concurrent programming is often fraught with peril:

  • Data races can occur when multiple threads write to the same data without the protection of either an atomic type or a locking mechanism.
  • Lifetime issues can occur when a thread outlives a variable declaration. The variable may be destroyed by the parent, while the child thread still relies on it.

Let’s compare some Rust and C++ code, to see how Rust protects you from these issues.

Finding Prime Numbers

There are many ways to factorize a number and detect if it is prime. Some algorithms are very fast — but for this article we’re going to use a deliberately slow approach. A simple algorithm uses more CPU time, better demonstrating the benefits of multi-threading. We’ll use the following function to determine if a number is prime:

is_prime iterates from 2 to half the number and ensures that all of the iterations return a remainder (the % operator) other than zero when the tested number is divided by the iterator.

To be sure that it works, here’s a quick unit test that checks a list of prime numbers under 100:

Here’s an equivalent C++ function:

I kept it simple; range support isn’t in very many shipping C++ compilers yet — using an iota view from the C++20 standard would enable a similar iterator-based approach. The code is functionally equivalent, however.

Single Threaded Prime Detection

The following Rust program counts how many prime numbers it can find between 2 and 200,000:

This program finds 17,984 prime numbers in the range 2 to 200,000.

An equivalent C++ program looks like this:

This program also finds 17,984 prime numbers. So far, so good. Let’s make a data race!

Racing Data

Looking to speed-up our prime counting, we eagerly fire up some C++ std::thread objects to divide our workload between two threads:

This program compiles and runs. Visual Studio (my compiler) doesn’t show any warnings. The code has split the workload in two, spawning two threads — each covering half of the prime counting.

Unfortunately, it doesn’t work. The first time I ran it, it counted 17,983 prime numbers. The second time, it counted only 17,981! Threads are concurrently accessing count. Incrementing a variable is a three-stage process: you read the current value, add one to it, and store the result. There’s no guarantee that all of these steps have been completed independently of other threads — resulting in a data race.

Maybe a Rust version of the same code will work?

The Rust version of the same program won’t compile. It gives two errors:

  • counter may not be mutably borrowed more than once.
  • counter may outlive it’s borrowed value. A lifetime issue.

Rust has detected that your program is unsafe and prevents you from creating code that inadvertently gives the wrong answer.

Safe Multi-Threading

Here’s a safe version in Rust that both compiles and gives the correct answer:

There are three new concepts in this program:

  • AtomicUSize is a special “atomic” type. Atomic types provide automatic protection against data-races. They are typically very fast, mapping directly to equivalent CPU instructions. Whenever you access an atomic type, you have to indicate the synchronization guarantee you need — in this case SeqCst provides the highest level of protection.
  • Arc is an “atomic reference counter”. When you wrap a type in an Arc, you can safely clone it and move the clones between threads. Each clone is a pointer to the contained data. Arc guarantees that the underlying data won’t be deleted until every reference is done using it.
  • The thread closures (inline functions) move their captured data. They are operating on clones of the counter Arc — so they all point to the same place.

Combining these concepts has protected us against both data-race and lifetime issues:

  • Rust’s borrow checker is happy because data is not borrowed between threads: the Arc is cloned, safely sharing a pointer to the counter data.
  • Rust’s data-race protection is satisfied that using an AtomicUsize cannot result in a data-race.

The result is a fast program that gives the correct answer every time.

Wrap-Up

Rust’s fearless concurrency guarantees saved us from a data-race! C++ didn’t warn about the error, it just gave incorrect results. Rust saved the day, by refusing to compile the incorrect code. In a large program, this could have saved hours of painstaking debugging.

Rust’s Arc and AtomicUSize types make for complicated-looking code. In the next part of this series, I’ll show you ways to tame the complexity and create fast, multi-threaded code that remains easy to read.

--

--