# C Stream Locking vs. OpenMP Critical Sections This document explains the difference between relying on the internal thread safety of standard I/O streams (`printf`) and enforcing code-level synchronization with OpenMP constructs. ## Core Takeaway - **`printf` internal locking** locks the `FILE` stream (`stdout`) for the duration of a **single function call**. Output from one call is never mixed into the middle of another call's output, but nothing keeps *separate* calls together. - **OpenMP `critical`** makes a **block of code** mutually exclusive among threads that enter a critical region of the same name. Multiple `printf` calls inside the block run as one uninterrupted sequence, relative to other threads using the same critical region. --- ## Approach A: Relying Only on `printf` Internal Locking POSIX requires stdio functions to behave as if they lock the stream internally. Each `printf` call acquires and releases that lock on its own. If one logical message is split across two `printf` calls, the lock is released between them. ```c // Thread 0 // Thread 1 printf("Thread_0_Part_A "); printf("Thread_1_Part_X "); /* stream lock released here */ /* stream lock released here */ printf("Thread_0_Part_B\n"); printf("Thread_1_Part_Y\n"); ``` **One possible outcome (interleaved at call boundaries):** ```text Thread_0_Part_A Thread_1_Part_X Thread_1_Part_Y Thread_0_Part_B ``` The order is nondeterministic, and this is just one of many valid interleavings. Each individual string stays intact, but the pieces from different threads can mix in any order. ![locking](images/figure1_printf_stream_lock.svg) **Figure 1:** Each printf call holds the stdout lock for its whole duration. Thread B blocks until Thread A releases the lock, so `Hello\n` and `World\n` each stay intact. Without this internal lock, individual characters could interleave. --- ## Approach B: Using an OpenMP `critical` Section Wrapping the multi-step print in `#pragma omp critical` makes the whole block mutually exclusive among threads using the same critical region. ![critical](images/omp_critical_section_sequence.svg) **Figure 2:** The critical gate is held across both printf calls. Thread 1 cannot enter until Thread 0 releases the gate, so each thread’s two parts stay together. Which thread enters first is nondeterministic. ```c #pragma omp critical(printing_gate) { printf("Thread_0_Part_A "); printf("Thread_0_Part_B\n"); } ``` **Resulting output (lines never mix, though thread order is still nondeterministic):** ```text Thread_0_Part_A Thread_0_Part_B Thread_1_Part_X Thread_1_Part_Y ``` Other threads that reach the same critical region wait until the current thread leaves the block. **Important caveats:** - An **unnamed** `critical` is a single global lock shared by *all* unnamed critical regions in the program, which can cause unintended serialization of unrelated code. A **named** critical (`critical(name)`) only excludes other regions with the same name. - `critical` only protects against threads that also go through the same gate. A thread that calls `printf` outside the critical region can still interleave its output. Every writer to `stdout` must use the same gate. --- ## Summary Matrix | Feature | A: `printf` lock only | B: OpenMP `critical` | | :--- | :--- | :--- | | **Lock mechanism** | Implicit, managed by the C standard library (e.g., glibc), using a per-`FILE` lock | Compiler emits calls into the OpenMP runtime library, which performs the locking | | **Scope of protection** | A single function call | A user-defined block (`{ ... }`) | | **What it guarantees** | Output of one call is not split by another thread's call | Block executes mutually exclusively among threads using the same critical region | | **What it does not guarantee** | Ordering or grouping across multiple calls | Protection from threads that bypass the gate (e.g., `printf` outside the block) | | **Overhead** | Low | Depends on block length; cost comes mainly from serializing threads waiting at the gate | --- ## Complete Example ```c // gcc -fopenmp main.c -o main #include #include int main() { // Set number of threads to 2 for clear observation omp_set_num_threads(2); printf("=== APPROACH 1: Relying ONLY on printf Internal Locking ===\n"); printf("Expected: The words might interleave because the lock releases between printfs.\n\n"); #pragma omp parallel { int id = omp_get_thread_num(); // Simulating printing a multi-part message if (id == 0) { printf("Thread_0_Part_A "); printf("Thread_0_Part_B\n"); } else { printf("Thread_1_Part_X "); printf("Thread_1_Part_Y\n"); } } printf("\n=== APPROACH 2: Using OpenMP Critical Construct ===\n"); printf("Expected: Entire blocks are guaranteed to stay together cleanly.\n\n"); #pragma omp parallel { int id = omp_get_thread_num(); // The critical pragma locks the entire block of code #pragma omp critical(clean_print) { if (id == 0) { printf("Thread_0_Part_A "); printf("Thread_0_Part_B\n"); } else { printf("Thread_1_Part_X "); printf("Thread_1_Part_Y\n"); } } } return 0; } ```