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.

// 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.

locking

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

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 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

// 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;
}