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
printfinternal locking locks theFILEstream (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
criticalmakes a block of code mutually exclusive among threads that enter a critical region of the same name. Multipleprintfcalls 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.
// 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):
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.
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.
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.
#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):
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
criticalis 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.criticalonly protects against threads that also go through the same gate. A thread that callsprintfoutside the critical region can still interleave its output. Every writer tostdoutmust use the same gate.
Summary Matrix
Feature |
A: |
B: OpenMP |
|---|---|---|
Lock mechanism |
Implicit, managed by the C standard library (e.g., glibc), using a per- |
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., |
Overhead |
Low |
Depends on block length; cost comes mainly from serializing threads waiting at the gate |
Complete Example
// gcc -fopenmp main.c -o main
#include <stdio.h>
#include <omp.h>
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;
}