Concurrency Notes
Concurrency Introduction
- Converts a single physical CPU into multiple virtual CPUs.
- Enables multiple programs to run simultaneously.
- Creates illusion of large private virtual memory for each process.
- Each program behaves as if it has its own memory.
- OS secretly multiplexes address spaces across physical memory and disk.
Threads
- A new abstraction for a single running process.
- Multi-threaded program has more than one point of execution (multiple PCs being fetched and executed from).
- Each thread is like a separate process but shares the same address space and can access the same data.
- Single thread state:
- Program counter (PC) tracks instruction fetching.
- Private set of registers for computation.
- Context switch between threads is similar to process context switch but address space remains the same.
- Thread context switch saves/restores register state using Thread Control Blocks (TCBs).
- Each thread has its own stack for local variables, arguments, and return values (thread-local storage).
Why Use Threads?
- Parallelism: Speed up programs on multi-processor systems by using a thread per CPU.
- Avoid Blocking: Prevent program progress from stalling due to slow I/O by switching to other threads.
- Enables overlap of I/O with other activities within a single program.
- Threads share an address space, making data sharing easier compared to multiple processes.
Thread Creation Example
- Program creates two threads, each printing “A” or “B”.
- Threads may run immediately or be put in a “ready” state.
- On a multiprocessor, threads could run simultaneously.
pthread_join()waits for a particular thread to complete.- The OS scheduler determines which thread runs at a given time.
- Thread creation is like a function call that creates a new thread of execution, running independently of the caller.
Shared Data and Race Conditions
- Threads accessing shared data can lead to race conditions.
- Example: Two threads incrementing a shared variable.
- The desired result may not be achieved due to interleaving of instructions.
- Race condition: Results depend on the timing of code execution.
- Critical section: Code that accesses shared variables and must not be concurrently executed by more than one thread.
- Mutual exclusion: Ensures that only one thread executes within the critical section at a time.
The Heart Of The Problem: Uncontrolled Scheduling
- Shows example assembly code for incrementing a counter:
mov 0x8049a1c, %eax
add $0x1, %eax
mov %eax, 0x8049a1c
- Shows example assembly code for incrementing a counter:
Race Condition Explanation
- A thread loads counter value to register, gets interrupted, another thread increments counter, first thread resumes and overwrites the incremented value.
- Critical section requires mutual exclusion to prevent race conditions.
- Indeterminate program: Output varies across different runs.
Wish For Atomicity
- Atomic operations solve race conditions by executing a series of actions as a single, uninterruptible unit (“all or nothing”).
- Atomicity ensures either all actions occur or none occur, with no intermediate state visible.
Synchronization Primitives
- Hardware provides instructions to build synchronization primitives.
- OS and hardware support are used to create multi-threaded code that accesses critical sections in a synchronized manner.
Need For Thread Interaction
- Threads often need to wait for another to complete an action.
- Mechanisms are needed to support sleeping/waking interactions in multi-threaded programs.
Key Concurrency Terms
- Critical Section: Code accessing a shared resource.
- Race Condition: Multiple threads enter a critical section simultaneously, leading to unexpected outcomes.
- Indeterminate Program: Program with race conditions, producing varying output.
- Mutual Exclusion: Primitives to ensure only one thread enters a critical section, avoiding races.
Concurrency in OS
- The OS was the first concurrent program and many techniques were created for use within the OS.
- Page tables, process lists, file system structures, and virtually every kernel data structure has to be carefully accessed, with the proper synchronization primitives, to work correctly.
Thread API Guidelines
- Keep it simple.
- Minimize thread interactions.
- Initialize locks and condition variables.
- Check your return codes.
- Pass arguments and return values carefully.
- Use condition variables to signal between threads.
- Use the manual pages.