Part 25: Concurrency

Every program so far has done one thing at a time. A computer can do several: its processor has a number of cores, and each can run its own line of execution — its own thread — at the same moment as the others. Splitting work across threads makes it finish sooner, and lets a program wait for one thing while it gets on with another.

The difficulty is not starting threads but letting them share. Two threads that change the same memory at once can each undo the other's work, and the result changes from one run to the next. This part starts with threads themselves and then works through the standard tools that keep them out of each other's way: the Thread package starts and joins them, Atomic changes one value safely, and Sync provides the locks, channels and counters, with Arc from Allocator to share ownership.

What you will learn

  • Starting a thread with Spawn, passing it a context, and collecting its result with Join.
  • What a data race is, and why output from several threads comes out in a different order every run.
  • Changing one shared value in indivisible steps with Atomic<T>, the CompareExchange retry loop, and memory orders.
  • Keeping several values consistent under a Mutex<T>, whose guard releases the lock for you.
  • Sharing ownership of one value between threads with Arc<T>.
  • Passing values from thread to thread through a bounded Channel<T>, and ending the stream with Close.
  • Waiting for a group of detached workers with WaitGroup, and the Semaphore and Barrier beside it.
  • One-time initialization with Once, and a thread var that every thread has its own copy of.
  • Letting many readers in at once with RwLock<T>.

Which tool when

flowchart TD
    q{"Do the threads<br/>share data?"}
    q -- "no: they pass it on" --> ch["`Channel`"]
    q -- "only read it" --> arc["`Arc`"]
    q -- "change it" --> one{"one number<br/>or several values?"}
    one -- "one number" --> at["`Atomic`"]
    one -- "several" --> rw{"mostly read?"}
    rw -- "yes" --> rwl["`RwLock`"]
    rw -- "no" --> mx["`Mutex`"]

Two more questions are about time rather than data. To wait until a group of threads has finished, use a WaitGroup — or simply Join each one. To do something exactly once, whichever thread asks first, use a Once.

Lessons

LessonWhat you will learn
25.1Threadstart a thread, wait for its result, and ask how many can run at once
25.2Atomiccount across threads without a lock
25.3Mutexguard shared data with a lock that only one thread holds at a time
25.4Arcshare ownership of one value between threads
25.5Channelsend values from one thread to another
25.6Wait groupwait until a group of threads has finished
25.7Oncerun initialization exactly once, whichever thread gets there first
25.8Read-write lockmany readers at once, or one writer alone

Before you start

Threads are given their work as a function and a context value, so this part leans on Callback, Struct and Move. Starting a thread can fail, which brings back the outcomes of Part 9: Errors. Locks hand out their value through a pointer and release themselves through a destructor, and the Arc lesson builds on Rc from Part 15: Memory. Each lesson's package is in the Examples repository's Concurrency/ folder:

cd Examples/Concurrency/Thread
rux run

Threads run in an order nobody chooses, yet every program in this part prints the same lines on every run. Each one prints from a single thread, after the others have finished — a habit worth keeping.

After this part

Part 26: Systems turns outward, to the world around the program: environment variables, child processes, the command line, logs and the network.

For the rules underneath, see Atomics and Module variables in the Rux Reference, and the Thread, Atomic and Sync packages in the API reference.