# Lecture 15 - Race Detection Source: https://wisconsin.twango.dev/sp26-cs537/lectures/lecture-15 # Lecture 15 - Race Detection **Instructor:** Tej Chajed **Topics:** Data races, race detection, ThreadSanitizer, happens-before relation, linearizability > [!abstract]- Learning Outcomes > - Define a data race > - Compare race detection with other testing strategies > - Interpret race detection output --- ## Finding Bugs in Concurrent Code Three main strategies for finding bugs in concurrent programs: ### Static Analysis - Concurrent code depends on context: what locks are held, what other threads are running - Static analysis tends to produce **false positives**: reports a bug but there is not one > [!note] Lecture Note > **False positives** = reports a bug, but code is correct. **False negatives** = does not report a bug, but code is incorrect. ### Running Many Times - Bug might not appear now, but after many months of running in production - Linux kernel has torture tests for synchronization primitives: high cost of testing due to importance ### Exhaustive Search - Run with a special scheduler that can force a specific interleaving - **Deterministic**: can re-run when we find a bug - **Exhaustive**: if there is a bug, we will find it - **Very slow** ### Systematic Concurrency Testing Lots of work on exploring thread interleaving schedules: - CHESS - Probabilistic testing - DPOR (Dynamic Partial-Order Reduction) - Also see loom and shuttle for Rust --- ## What Bugs Should We Look For? - Test result failures - Linearizability - Internal assertion violations - **Data races** (this lecture) > [!note] Lecture Note > Example with locks: T1 calls `lock()`, `unlock()`, `unlock()` while T2 calls `lock()`. If T2 calls `lock()` between T1's two `unlock()` calls, there is no mutual exclusion — this is a bug where the lock discipline is violated. --- ## Data Races > **Data Race:** Simultaneous reads/writes to the same memory location, at least one of which is a write. In C, a data race is always a bug (undefined behavior). > [!note] Lecture Note > read/write to the same location = data race. write/write to the same location = data race. Only read/read is safe. ### Example Bug ```c int shared_counter = 0; void *increment() { for (int i = 0; i < iters; i++) { shared_counter++; } } pthread_t t1, t2; pthread_create(&t1, NULL, increment, NULL); pthread_create(&t2, NULL, increment, NULL); ``` Both threads increment `shared_counter` without synchronization — this is a data race. ### Fix: Add a Lock ```c int shared_counter = 0; pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; void *increment() { for (int i = 0; i < iters; i++) { lock(&m); shared_counter++; unlock(&m); } } ``` --- ## ThreadSanitizer: Race Detection ### How It Works Idea: hook into the compiler and instrument memory operations. What needs to be instrumented: - Memory reads/writes - Lock operations - Synchronization (e.g., signal/wait) Compile with `clang -fsanitize=thread` to enable ThreadSanitizer. ### Race Condition Example 1 ```c // Global - we will fail to synchronize access to this int shared_counter = 0; // Thread function that increments the counter void *increment_thread(void *arg) { int iterations = *(int *)arg; for (int i = 0; i < iterations; i++) { // BUG: this is a race when run concurrently shared_counter++; } return NULL; } ``` ### Race Condition Example 2 ```c int shared_data = 0; int ready = 0; void *writer_thread() { shared_data = 42; ready = 1; return NULL; } void *reader_thread() { // DATA RACE: Reading 'ready' and 'shared_data' without synchronization while (!ready) { // spin loop } printf("Reader shared_data: %d\n", shared_data); return NULL; } ``` ### Reading ThreadSanitizer Output ```text Demo 1: Counter increment race ================ WARNING: ThreadSanitizer: data race (pid=24344) Write of size 4 at 0x0001008f4000 by thread T2: #0 increment_thread tsan_demo.c:20 (tsan_demo:arm64+0x100000690) Previous write of size 4 at 0x0001008f4000 by thread T1: #0 increment_thread tsan_demo.c:20 (tsan_demo:arm64+0x100000690) Location is global 'shared_counter' at 0x0001008f4000 (tsan_demo+0x100008000) Thread T2 (tid=41794395, running) created by main thread at: #0 pthread_create (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x2f708) #1 main tsan_demo.c:54 (tsan_demo:arm64+0x1000007f4) Thread T1 (tid=41794394, finished) created by main thread at: #0 pthread_create (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x2f708) #1 main tsan_demo.c:54 (tsan_demo:arm64+0x1000007f4) SUMMARY: ThreadSanitizer: data race tsan_demo.c:20 in increment_thread ================ Expected: 20000, Got: 20000 ``` Key parts of the output: - **WARNING line**: identifies it as a data race - **Write/Previous write**: shows the two conflicting accesses, with source file and line number - **Location**: identifies the variable involved (`shared_counter`) - **Thread creation**: shows where each thread was created - **SUMMARY**: one-line summary with file, line, and function --- ## How ThreadSanitizer Detects Races ### Idea 1: Track Lock Sets - For each memory location, record what locks are held when it is accessed - It is safe to have simultaneous (unordered) access to the same address if both threads hold the same lock > [!note] Lecture Note > Example: Thread 1 does `lock(l)`, `read(p)`, `unlock(l)`. Thread 2 does `lock(l)`, `write(p)`, `unlock(l)`. Both hold lock `l` when accessing `p`, so this is safe — no race reported. ### Idea 2: Track Simultaneous Events - When the program runs, there is one sequence of operations - Want to know if events are unsynchronized and could happen in reverse order ### The Happens-Before Relation Consider events E1 and E2 in the program (memory reads/writes to specific addresses): - **E1 < E2** means E1 "happens before" E2 - If **E1 does not happen before E2** and **E2 does not happen before E1**, then E1 and E2 are **simultaneous** > [!note] Lecture Note > Example: `a = 1` then `x = a` — within the same thread, the first happens before the second (program order). ### Happens-Before Example From "ThreadSanitizer — data race detection in practice": | Thread T1 | Thread T2 | Thread T3 | |-----------|-----------|-----------| | S1 | S2 | S3 | | Signal(H1)| | | | S4 | Wait(H1) | | | | S5 | | | | Signal(H2)| | | | S6 | Wait(H2) | | | | S7 | The paper uses S for **segments** — sequences of events from one thread. - **S1 < S4** (same thread — program order) - **S1 < S5** (happens-before arc: Signal_T1(H1) -> Wait_T2(H1)) - **S1 < S7** (happens-before is transitive) - **S4 and S2 have no relation** (they are simultaneous) > [!note] Lecture Note > S1 < S4 is due to program order. S1 < S5 is due to signaling (Signal on H1 in T1 happens before Wait on H1 in T2). ### Putting It Together: Reporting a Bug If E1 and E2 for the same memory location are **simultaneous**, at least one is a write, and they hold **no common lock**: report a data race. Some sophisticated tracking is used to report *where* the data race is. --- ## False Positives in Race Detection ```c // Thread1 // Thread2 obj->UpdateMe(); mu.Lock(); mu.Lock(); bool f = flag; flag = true; mu.Unlock(); mu.Unlock(); if (f) obj->UpdateMe(); ``` No common locks protect `obj->UpdateMe()` — ThreadSanitizer reports a race. But the code is actually correct: `flag` starts as `false`, so Thread2 only calls `obj->UpdateMe()` if `flag` is `true`, which means Thread1 has already finished its call. > [!note] Lecture Note > `flag = false` initially. Thread2 reads `flag` under the lock. If `flag` is still false, Thread2 never calls `UpdateMe()`. The race detector does not understand this application-level logic. ### Pure Happens-Before Add additional happens-before arcs: **Unlock_T1(L) < Lock_T2(L)** Reduces false positives, but might miss bugs (more false negatives). --- ## Issues with Race Detection - **Slow**: ThreadSanitizer typically makes programs 3-5x slower - **False positives**: benign races, synchronization the tool does not understand - **False negatives**: does not try every possible execution --- ## Linearizability Consider a map with `Get(k)` and `Put(k, v)` operations. If you access it concurrently, what do you expect? > **Linearizability (informal):** Each operation appears to execute at one instant in time between invocation and return. > > **Linearizability (more precise):** For each trace, you can find a linearization (an ordering of operations) consistent with observed return values. ### Linearizability Testing Tools: Jepsen, Porcupine > [!note] Lecture Note > In the Porcupine visualization, a `put('200')` operation spans a time range. A `get()` returning `'200'` is valid if the put appeared to execute before it. A `get()` returning `'0'` after the put completed would be invalid (the put must have taken effect by then). --- ## Concurrency Review - Use threads to take advantage of multiple cores - Correctness is challenging: many thread interleavings - Locks and condition variables - Bugs: atomicity violation, ordering problems, deadlocks - Deadlock prevention: lock ordering, atomic lock acquisition, lock-free code --- ## Related - **Navigation:** [[lecture-14|← Lecture 14]] · [[lecture-16|Lecture 16 →]] - **Exam:** [[exams/midterm-2/index|Midterm 2]] - **Course:** [[README|CS 537 Map of Content]]