Chapter 10: Processes, threads, and containers
The introduction distinguishes concurrency from parallelism and explains how a single CPU core can support concurrent tasks.
After this chapter you will be able to
- Concurrency and parallelism distinction. Distinguish concurrency, which enables multiple tasks to be carried out at the same time, from parallelism, which uses multiple physical CPU cores at the same time.
- Concurrency on one core. Explain how a system can be concurrent on a single CPU core when threads waiting for memory or I/O data are blocked and rescheduled once the data is available.
- Lambda syntax and closures. Describe how Rust writes a lambda function and why it is a closure.
- Threads and concurrent execution. Explain that operating systems use threads to enable concurrent execution, give each thread fair access to the CPU, and that spawning threads is a fundamental skill for using multi-core CPUs.
- Ownership of captured variables. Explain why a spawned thread’s closure often needs the move keyword when it captures variables from its parent scope: the thread may outlive that scope, so ownership moves into the closure to keep access valid.
- Joining a spawned thread. Explain that a spawned thread forks from its parent and that calling join() makes the calling thread wait until the other thread finishes.
- Few-thread creation overhead. Explain what the comparison with the single-threaded program suggests about the cost of spawning one or two threads, and why the result is not a definitive benchmark.
- Limits of thread scaling. Explain why performance can deteriorate as the number of threads grows, even though spawning a few threads had little impact in the example.
- Thread costs and sleep variability. Explain that spawning and switching between threads have costs, and describe what happened to the measured duration of a 20 ms sleep as batches grew.
- Physical core scaling limit. Explain how the spin-loop benchmark illustrates the scaling limit for CPU-intensive multithreading.
- Ownership When Joining Handles. Explain why the code removes thread handles from the vector before joining them, and why iterating over the vector by value also works.
- Yielding Versus CPU Hints. Distinguish what yield_now() asks the OS to do from what spin_loop_hint() signals to the CPU, including their stated limitations.
- Parent-Scope Variables in Closures. Explain that the attempted use of one variable in multiple thread closures produces a compiler error, and that adding `move` where the closures are created switches them to move semantics and relies on `Copy`.
- Closure representation and captures. Explain that the compiler represents a closure as an anonymous struct implementing FnOnce and potentially Fn or FnMut, and that the struct contains variables it uses from its surrounding environment.
- Function pointers and capture exception. Distinguish a function's representation as a pointer to executable code from a closure that captures variables, and describe the exception for closures that enclose no environmental variables.
- Username-based avatar generation. Explain how the application uses a username and its hash digest to generate a default avatar, and describe how Base 16 characters serve as drawing instructions.
- Render-hex input and output. Describe how a stream of valid Base 16 bytes produces a unique image, using the SHA256 output for “Rust in Action” as the example. State how to view the generated SVG file.
- Render-hex conversion pipeline. Trace the four stages from standard input to an SVG file, and explain why the program separates pen-movement operations from SVG path data.
- Artist and SVG path data. Explain how Artist, convert(), and generate_svg() connect pen operations to the finished drawing.
- Functional parse refactor. Describe how map() and collect() replace the for loop and temporary steps variable in parse().
- Rayon data parallelism. Describe how the section introduces Rayon to process different data with multiple threads, including the role of its prelude and parallel iterator methods.
- Task Queue Work Distribution. Explain why a task queue is useful when work does not come from a tidy iterator: tasks can originate anywhere, while worker threads take new tasks after finishing their current ones.
- Typed Channel Messages. Explain how a channel’s sending and receiving ends pass messages, and distinguish those typed messages from an unstructured byte stream.
- Render Hex Result Ordering. Explain why introducing independent threads complicates the render-hex task queue and what change the section describes for its command vector.
- Task isolation trade-offs. Explain what isolation protects tasks from and why stronger isolation generally costs more.
- Concurrency task vocabulary. Distinguish programs, processes, threads, and coroutines as used here, and distinguish concurrency from parallelism.
- Thread scheduling and isolation. Explain that threads are the lowest level of isolation the OS understands and can schedule, while smaller forms of concurrency are invisible to it and have their task switching managed by the process. State that both require context switching.
- Context switches and their cost. Explain that a context switch moves between tasks at the same level of virtualization and that switching threads can require CPU and OS state changes. Describe how greater isolation raises the cost of switching.
- Reasons CPUs switch tasks. Explain why a CPU switches tasks despite executing instructions serially: it can resume a task from a saved spot, and waiting for memory, disk, or network data leaves time to do other work.
- Process memory isolation. Explain that threads exist within a process, while a process has memory independent of other processes and protected by the OS together with the CPU.
- Wasm task isolation. Explain why WebAssembly isolates tasks within a process and how its treatment of code as potentially hostile relates to third-party dependencies.
- Linear memory translation. Describe how a Wasm module’s linear memory relates to the process’s address space and how the runtime handles a request for data.
- Container resource isolation. Explain how containers extend processes with OS-provided isolation of resources beyond the address space, using filesystems and networks as examples, and identify namespaces as the term for those protections.
- OS support and unikernels. Explain the limitations of running without an OS and describe how a unikernel provides a middle ground by pairing one application with a stripped-down OS.
What it builds on
- Defining functions (chapter 2) · Assumes familiarity with regular function definitions.
- How ownership moves (chapter 4) · Introduces how ownership moves between parts of a program.
- Defining and making use of an enum (chapter 3) · Enums as Rust types that can represent alternatives.
- Virtual memory (chapter 6) · The distinction between a process’s address space and virtual memory.
Section by section
Introduction
The introduction distinguishes concurrency from parallelism and explains how a single CPU core can support concurrent tasks.
- Concurrency and parallelism distinction: Distinguish concurrency, which enables multiple tasks to be carried out at the same time, from parallelism, which uses multiple physical CPU cores at the same time.
Why it matters: The distinction helps you describe concurrent programs without assuming they need multiple CPU cores. - Concurrency on one core: Explain how a system can be concurrent on a single CPU core when threads waiting for memory or I/O data are blocked and rescheduled once the data is available.
Why it matters: Waiting for data need not prevent a single-core system from handling concurrent tasks.
Key terms: processes, threads, containers, concurrency, parallelism, single CPU core, blocked state, rescheduled, memory, I/O
10.1 Anonymous functions
Introduces Rust lambda functions as locally defined closures and compares one with a regular function.
- Lambda syntax and closures: Describe how Rust writes a lambda function and why it is a closure.
Why it matters: This syntax appears in the chapter’s upcoming examples of concurrent code. - Local lambda definition: Distinguish where a lambda can be defined from where a regular function can be defined, using the section’s comparison as an example.
Why it matters: It explains why the example places its lambda inside main rather than alongside the regular function.
Key terms: lambda functions, closures, vertical bars, global scope, main(), regular function, lambda function
10.2 Spawning threads
Introduces threads as the operating system’s main way to enable concurrent execution and explains why spawning them matters for using multi-core CPUs.
- Threads and concurrent execution: Explain that operating systems use threads to enable concurrent execution, give each thread fair access to the CPU, and that spawning threads is a fundamental skill for using multi-core CPUs.
Why it matters: This explains why creating threads is useful before the chapter shows how to do it.
Key terms: threads, concurrent execution, spawning, CPU, multi-core CPUs
10.2.1 Introduction to closures
This section introduces the anonymous functions passed to thread::spawn and explains why captured values often need to move into a spawned thread’s closure.
- Thread closure syntax: Describe the anonymous function syntax typically passed to std::thread::spawn(): two vertical bars for arguments followed by a body in curly brackets, with no arguments supplied to spawn in this example.
Why it matters: Recognizing this syntax makes thread-spawning examples easier to read. - Ownership of captured variables: Explain why a spawned thread’s closure often needs the move keyword when it captures variables from its parent scope: the thread may outlive that scope, so ownership moves into the closure to keep access valid.
Why it matters: This explains the lifetime-related compiler complaint that can appear when a thread uses an outer variable.
Key terms: anonymous function, std::thread::spawn(), capture, move, ownership, closure, calling scope
10.2.2 Spawning a thread
The section uses a sleeping subthread to explain what joining a spawned thread means.
- Joining a spawned thread: Explain that a spawned thread forks from its parent and that calling join() makes the calling thread wait until the other thread finishes.
Why it matters: It explains why a thread that calls join() does not continue immediately.
Key terms: spawned, forked, join, join(), calling thread
10.2.3 Effect of spawning a few threads
This section uses a two-thread timing example to show that spawning a few threads has little measured overhead, while explaining why large numbers of threads can slow down.
- Few-thread creation overhead: Explain what the comparison with the single-threaded program suggests about the cost of spawning one or two threads, and why the result is not a definitive benchmark.
Why it matters: It challenges the assumption that creating even a few threads is necessarily expensive. - Limits of thread scaling: Explain why performance can deteriorate as the number of threads grows, even though spawning a few threads had little impact in the example.
Why it matters: It distinguishes the low cost of a few threads from the costs of running many.
Key terms: spawning a thread, negligible impact, benchmark suite, threads don’t scale, OS scheduler, memory
10.2.4 Effect of spawning many threads
This section uses sleep and spin-loop measurements to show the costs of spawning many threads, the limits of timing accuracy, and the poor scaling of CPU-intensive threads beyond the number of physical cores.
- Thread costs and sleep variability: Explain that spawning and switching between threads have costs, and describe what happened to the measured duration of a 20 ms sleep as batches grew.
Why it matters: Creating more threads can make a program’s timing less predictable rather than simply giving it more capacity. - Short pause timing strategies: Distinguish the observed timing of the spin loop from OS sleep for short pauses, and state the section’s suggested timing strategies.
Why it matters: The choice between sleeping and spinning can affect how closely a short pause matches its intended duration. - Physical core scaling limit: Explain how the spin-loop benchmark illustrates the scaling limit for CPU-intensive multithreading.
Why it matters: Adding CPU-intensive threads beyond the available physical cores may quickly hurt performance.
Key terms: spawning threads, switching between threads, caches, spin loop, sleep, CPU-intensive multithreading, physical cores, hyper-threading
10.2.5 Reproducing the results
The section explains the waiting strategies used to generate thread measurements, how to join stored thread handles, and what yielding does during a busy wait.
- Thread Waiting Strategies: Distinguish the 20 ms sleep used in listing 10.4 from the busy wait used in listing 10.5, and describe how to collect their output for analysis.
Why it matters: The different ways threads wait are the basis for the measurements discussed here. - Ownership When Joining Handles: Explain why the code removes thread handles from the vector before joining them, and why iterating over the vector by value also works.
Why it matters: It explains both the unusual join loop and the simpler ownership-based alternative. - Yielding Versus CPU Hints: Distinguish what yield_now() asks the OS to do from what spin_loop_hint() signals to the CPU, including their stated limitations.
Why it matters: These calls affect what happens while a spinning thread waits, without providing a guaranteed wake-up time.
Key terms: sleep, busy wait, busy loop, spin loop, join(), handlers, pop(), ownership, reference, std::thread::yield_now()
10.2.6 Shared variables
This subsection shows that adding `move` to closures fixes an attempted use of a parent-scope variable in multiple threads, relying on `Copy`.
- Parent-Scope Variables in Closures: Explain that the attempted use of one variable in multiple thread closures produces a compiler error, and that adding `move` where the closures are created switches them to move semantics and relies on `Copy`.
Why it matters: It connects a compiler error in the threading example to the closure change that fixes it.
Key terms: move keyword, closures, move semantics, Copy
10.3 Differences between closures and functions
This section explains why Rust closures and functions are not always interchangeable by contrasting their representations and the special case of closures that capture no variables.
- Closure representation and captures: Explain that the compiler represents a closure as an anonymous struct implementing FnOnce and potentially Fn or FnMut, and that the struct contains variables it uses from its surrounding environment.
Why it matters: A closure can carry surrounding data with it, unlike an ordinary function pointer. - Function pointers and capture exception: Distinguish a function's representation as a pointer to executable code from a closure that captures variables, and describe the exception for closures that enclose no environmental variables.
Why it matters: The no-capture exception helps explain when a closure can be used like a function pointer.
Key terms: closure, anonymous struct, std::ops::FnOnce, std::ops::Fn, std::ops::FnMut, environment, function pointer, code
10.4 Procedurally generated avatars from a multithreaded parser and code generator
Introduces an application that generates distinctive default avatars from usernames using a small drawing language.
- Username-based avatar generation: Explain how the application uses a username and its hash digest to generate a default avatar, and describe how Base 16 characters serve as drawing instructions.
Why it matters: This connects the avatar’s input data to the kind of image the application produces.
Key terms: digest, hash function, procedural generation, parallax lines, Base 16 alphabet, opcodes
10.4.1 How to run render-hex and its intended output
The subsection describes render-hex’s intended image output and how to view it.
- Render-hex input and output: Describe how a stream of valid Base 16 bytes produces a unique image, using the SHA256 output for “Rust in Action” as the example. State how to view the generated SVG file.
Why it matters: This tells you what render-hex produces and how to inspect the result.
Key terms: render-hex, Base 16 bytes, SVG
10.4.2 Single-threaded render-hex overview
This section explains how the single-threaded render-hex program turns hexadecimal input into an SVG drawing through a staged pipeline.
- Render-hex conversion pipeline: Trace the four stages from standard input to an SVG file, and explain why the program separates pen-movement operations from SVG path data.
Why it matters: The stages identify the work that later versions of render-hex can reorganize. - Hexadecimal input operations: Describe how input bytes become virtual-pen operations, distinguishing what numerals and letters do.
Why it matters: This explains what the parser supplies to the rest of the drawing pipeline. - Artist and SVG path data: Explain how Artist, convert(), and generate_svg() connect pen operations to the finished drawing.
Why it matters: It connects the program's pen model to the path data that an SVG viewer draws.
Key terms: render-hex, STDIN, pipeline, main(), SVG, Operation enum, virtual pen, Artist, heading, convert()
10.4.3 Spawning a thread per logical task
The section refactors render-hex’s parse() function to use iterators, then shows how Rayon can process its data in parallel.
- Functional parse refactor: Describe how map() and collect() replace the for loop and temporary steps variable in parse().
Why it matters: This refactor makes the parsing work expressible as iterator operations. - Iterator optimization opportunity: Explain why expressing parse() with iterator methods gives the Rust compiler more room to optimize than specifying every step with a for loop.
Why it matters: The change is more than shorter syntax: it leaves optimization choices to the compiler. - Rayon data parallelism: Describe how the section introduces Rayon to process different data with multiple threads, including the role of its prelude and parallel iterator methods.
Why it matters: Rayon provides the section’s route from iterator-based parsing to parallel processing.
Key terms: map(), collect(), Vec<T>, closure, iterators, rayon, data parallelism, prelude, par_iter(), par_bytes()
10.4.4 Using a thread pool and task queue
This section explains how channels can connect task producers to worker threads and applies a channel-based task queue to render-hex.
- Task Queue Work Distribution: Explain why a task queue is useful when work does not come from a tidy iterator: tasks can originate anywhere, while worker threads take new tasks after finishing their current ones.
Why it matters: This is the reason the section moves from a parallel iterator to a task queue. - Shared Data Locking Alternative: Describe how Arc<Mutex<T>> could protect shared task or result vectors, and why the section turns to channels instead.
Why it matters: It clarifies what shared-state problem channels offer another way to handle. - Typed Channel Messages: Explain how a channel’s sending and receiving ends pass messages, and distinguish those typed messages from an unstructured byte stream.
Why it matters: Typed senders and receivers are the building blocks of the section’s task queue. - Channel Capacity And Directions: Describe the bounded-queue trade-off given for crossbeam channels and how the section models two-way communication.
Why it matters: Capacity affects whether senders must wait, while communication direction determines how many channels the example uses. - Render Hex Result Ordering: Explain why introducing independent threads complicates the render-hex task queue and what change the section describes for its command vector.
Why it matters: It identifies the ordering complication that the task-queue version must accommodate.
Key terms: task queue, worker threads, Arc<Mutex<T>>, Mutex, Arc, channel, Sender<T>, Receiver<T>, send(), recv()
10.5 Concurrency and task virtualization
Introduces concurrency terminology and explains how stronger task isolation trades greater protection for greater overhead.
- Task isolation trade-offs: Explain what isolation protects tasks from and why stronger isolation generally costs more.
Why it matters: It helps explain why different ways of running tasks offer different protections and costs. - Concurrency task vocabulary: Distinguish programs, processes, threads, and coroutines as used here, and distinguish concurrency from parallelism.
Why it matters: These terms identify which level of work is being discussed when comparing concurrency models. - Non-blocking asynchronous I/O: Explain how non-blocking I/O and asynchronous programming let a thread do other work while awaiting input, and why the section says this can help I/O-heavy workloads.
Why it matters: It connects the I/O vocabulary to the section’s broader comparison of concurrency costs.
Key terms: isolation, task virtualization, runtime overhead, program, process, thread of execution, coroutine, task, concurrency, non-blocking I/O
10.5.1 Threads
This subsection distinguishes OS-scheduled threads from smaller forms of concurrency whose task switching a process manages itself.
- Thread scheduling and isolation: Explain that threads are the lowest level of isolation the OS understands and can schedule, while smaller forms of concurrency are invisible to it and have their task switching managed by the process. State that both require context switching.
Why it matters: This distinction clarifies which concurrent tasks the OS can schedule and which a program must manage itself.
Key terms: thread, coroutines, fibers, green threads, context switching
10.5.2 What is a context switch?
This section explains what a context switch is, why it has a cost, and why CPUs switch tasks while programs wait for data.
- Context switches and their cost: Explain that a context switch moves between tasks at the same level of virtualization and that switching threads can require CPU and OS state changes. Describe how greater isolation raises the cost of switching.
Why it matters: It helps explain why switching between tasks is not free. - Reasons CPUs switch tasks: Explain why a CPU switches tasks despite executing instructions serially: it can resume a task from a saved spot, and waiting for memory, disk, or network data leaves time to do other work.
Why it matters: It explains how switching tasks makes use of time that would otherwise be spent waiting for data.
Key terms: context switch, virtualization, isolation, CPU registers, CPU caches, tasks, CPU, memory, disk, network
10.5.3 Processes
This subsection explains how processes isolate memory and why sharing data between them requires operating-system support.
- Process memory isolation: Explain that threads exist within a process, while a process has memory independent of other processes and protected by the OS together with the CPU.
Why it matters: Memory isolation explains why processes cannot share data in the same way threads can. - Interprocess data sharing: Explain why Rust channels and data protected by Arc<Mutex<_>> do not suffice for sharing data between processes, and describe the OS-supported alternatives named here.
Why it matters: The memory boundary determines which communication tools can work between processes.
Key terms: threads, process, OS, CPU, Rust channels, Arc<Mutex<_>>, network sockets, interprocess communication (IPC)
10.5.4 WebAssembly
This section explains how WebAssembly isolates tasks within a process by giving modules linear memory rather than direct access to the process’s addresses.
- Wasm task isolation: Explain why WebAssembly isolates tasks within a process and how its treatment of code as potentially hostile relates to third-party dependencies.
Why it matters: This explains why Wasm provides isolation even between tasks in the same process. - Linear memory translation: Describe how a Wasm module’s linear memory relates to the process’s address space and how the runtime handles a request for data.
Why it matters: Linear memory explains how a module can use memory without seeing the process’s addresses.
Key terms: WebAssembly, Wasm, process boundary, tasks, linear memory, runtime, virtual memory, address space
10.5.5 Containers
This section distinguishes containers from processes by the additional OS-provided isolation of resources such as filesystems and networks.
- Container resource isolation: Explain how containers extend processes with OS-provided isolation of resources beyond the address space, using filesystems and networks as examples, and identify namespaces as the term for those protections.
Why it matters: It clarifies what extra isolation a container provides beyond a process.
Key terms: containers, processes, filesystem, network, namespaces, address space
10.5.6 Why use an operating system (OS) at all?
This section explains what it means to run an application without an operating system, what support that gives up, and how a unikernel offers a middle ground.
- Freestanding application meaning: Explain that a freestanding application runs without OS support and that embedded developers use freestanding binaries when no OS is available.
Why it matters: It identifies the alternative to relying on an OS. - OS support and unikernels: Explain the limitations of running without an OS and describe how a unikernel provides a middle ground by pairing one application with a stripped-down OS.
Why it matters: It explains both why an application might need OS support and how a unikernel limits that support to what the application uses.
Key terms: freestanding, freestanding binaries, virtual memory, multithreading, unikernel
10.6 Summary
Where this is used later
- A fledgling operating system (FledgeOS) (chapter 11) · Prior discussion of processes as a way to organize work.
Concepts explained in this chapter
- threadA thread of execution is a sequence of CPU instructions intended to execute serially within a running program.
- concurrencyConcurrency is the execution of multiple computational activities during overlapping time periods, potentially on the same or different processors.
- workflowA process is a running program instance with its own execution context and operating-system-managed resources, including an address space.
- parallelismParallelism is the simultaneous execution of computational activities on multiple physical processing cores or other execution units.
- backpressureBack pressure is a flow-control mechanism that slows or blocks producers when a consumer or buffer cannot keep up.
- bounded queueA bounded queue is a queue with a fixed capacity that can make producers wait when no space is available.
- channelA channel is a communication abstraction with sending and receiving endpoints that transfers typed values between concurrent activities.
- clientA client is a network participant that initiates requests or sends messages to a service or peer.
- crossbeamcrossbeam is a Rust library providing concurrency primitives and utilities, including message-passing channels.
- select macroThe select! macro is a Crossbeam macro that waits for one of multiple channel operations to become ready.
- work queue patternA task queue is a pattern in which tasks are submitted independently of their processing and are later retrieved by worker threads.
- isolationIsolation is the separation of program execution or resources so that activity in one environment is restricted from directly affecting another.
- address spaceAn address space is the set of memory addresses available for identifying stored data and instructions in an execution environment.
- closureA closure is an anonymous callable value that can contain an expression body and capture values from its surrounding scope.
- asynchronous programmingAsynchronous programming is a programming model in which externally occurring events influence the order of control-flow execution.
- coroutineA coroutine is a task-switching concurrency mechanism in which the program, rather than the operating system, controls switching within a thread.
- non-blocking I/ONon-blocking I/O allows a thread to continue executing while it waits for input or output data.
- taskA task is an abstract unit of work whose meaning may range from a thread to a function call or a running program.
- busy-waiting loopA spin loop repeatedly checks a condition while actively consuming CPU time instead of blocking execution.
- sleepSleep is an operation that suspends the current thread for a specified duration or until an external wake-up event.
- pipelineA pipeline is a sequence of processing stages in which each stage transforms data for the next stage.
- moveMove semantics define how transferring ownership of a value invalidates or relocates its previous binding instead of implicitly copying the value.
- CPUA CPU is the computer hardware component that executes instructions and performs arithmetic and logical operations.
- function pointer typeA function pointer is a value that identifies a function and can be passed or stored so that the function can be invoked indirectly.
- bytesA byte is a unit of digital information consisting of eight binary bits.
- opcodeAn opcode is a symbolic code that identifies an instruction in a programming language or instruction set.
- memoryRAM is volatile computer memory that stores data and instructions available to a running processor.
- literalA literal form is Rust syntax that directly initializes or denotes a value according to the type's literal representation.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
