Redux on Ticket Agent Example
- The ticker agent example from the last slide deck is our most involved so far.
- We rely on global variables to store data that needs to be shared with all the threads. (Threading is often used to split the processor into multiple lightweight processes that collectively work toward a common goal, and the ticket agent example is an example that does just that).
- Some portions of the code cannot be partially executed and then pulled from the processor, because doing so introduces a synchronization issue called a race condition if some other thread gets processor time and make partial progress through the same block of code.
- Even individual C++ expressions as simple as remainingTickets--; aren't always atomic, because they may compile to two or more assembly code instructions. Individual assembly code instructions are always atomic, but groups of two or three instructions aren't guaranteed to be executed in sequence within the same time slice.
- Blocks of code (or even single expressions) that must be executed in full without competition from other threads are called critical regions.
Redux on Ticket Agent Example (continued)
- How did we fix?
- We introduced the notion of a mutex, which provides atomic methods called lock and unlock.
- If a mutex is unlocked (as it's constructed to be), then a call to lock flips the mutex into a locked state and returns without blocking or yielding.
- If a mutex is locked by some other thread, then another thread's attempt to lock the same mutex causes that thread to block indefinitely (within the lock call) until the thread that owns the lock on the mutex releases it by calling unlock.
- lock and unlock are used generally used to mark the beginning and end of a critical region, because doing so guarantees that at most one thread is executing any part of that critical region at one time.
It's only after that thread exits the critical region (and properly calls unlock) that some other thread is able to acquire the lock on the same mutex and enter the same critical region.
- The overall effect is that at most one thread is in the critical region at one time, so that all code within the critical region is effectively executed as one atomic transaction.
- The implementations of lock and unlock rely on privileged OS access (e.g. the ability to turn off interrupts) and/or a dedicated assembly code instruction (e.g. test-and-set) to ensure that they themselves are implemented to run atomically without threat of any race conditions.
- In general, it's considered good manners to keep the critical regions as small as is reasonable without introducing any concurrency issues.