22.1 Concurrency Fundamentals: Concepts, Processes, and Threads

22.1.1 Understanding Concurrency

Concurrency is the concept of structuring a program as multiple independent tasks that can execute overlapping in time. On systems with a single CPU core, this overlap is achieved by the operating system rapidly switching between tasks (interleaving), creating the illusion of simultaneous execution. On multi-core systems, concurrency can lead to parallelism, where tasks truly execute simultaneously on different cores, potentially reducing overall execution time.

Writing correct concurrent programs requires careful management of shared resources to prevent common problems:

  • Race Conditions: Occur when the program’s outcome depends on the unpredictable sequence or timing of operations (particularly reads and writes) performed by different threads on shared data. A specific type, the data race, involves concurrent, unsynchronized access to the same memory location where at least one access is a write.
  • Deadlocks: Occur when two or more threads are blocked indefinitely, each waiting for a resource that is held by another thread within the same cycle of dependencies.

In C and C++, preventing, detecting, and fixing these issues often relies heavily on programmer discipline, code reviews, and runtime analysis tools, as the compiler offers limited assistance. Data races, in particular, lead to undefined behavior. Rust fundamentally changes this dynamic. Its ownership and borrowing rules, enforced at compile time, guarantee that data races cannot occur in safe Rust code. Any code attempting unsynchronized access that could lead to a data race will simply fail to compile.

22.1.2 Processes vs. Threads

Two primary abstractions for concurrent execution provided by operating systems are processes and threads:

  • Processes: An instance of a running program. Each process typically has its own independent virtual address space, file descriptors, and other system resources allocated by the OS. Communication between processes (Inter-Process Communication or IPC) is mediated by the OS using mechanisms like pipes, sockets, or shared memory segments. This isolation provides safety but incurs overhead for context switching and communication.
  • Threads (specifically, OS threads or kernel threads): Represent independent execution paths within a single process. Threads belonging to the same process share the same virtual address space (including code, heap, and global variables) and resources like file descriptors. This shared environment facilitates easy data exchange but significantly increases the risk of data races if mutable data is accessed without proper synchronization. Thread context switching is generally less expensive than process context switching.

Rust’s standard library focuses on thread-based concurrency, providing primitives that integrate with the language’s safety features. Types like Mutex<T>, RwLock<T>, and the atomic reference counter Arc<T> leverage the type system to enforce safe access patterns to shared data, preventing data races at compile time – a stark contrast to the manual synchronization required in C/C++ where mistakes easily lead to runtime errors.