When people ask me what makes OpenMP different from writing raw threaded code, my answer is always the same: directives. Everything in OpenMP — parallelism, work distribution, and especially synchronization — is expressed through compiler directives (pragmas in C/C++, comment-based directives in Fortran) rather than explicit function calls scattered through your code. That design choice has a huge effect on how synchronization actually gets implemented and reasoned about, and I want to walk through the full set of synchronization directives and what each one is really doing under the hood.
Why Directives Instead of Function Calls
In POSIX threads, synchronization is done through explicit API calls: pthread_mutex_lock(), pthread_cond_wait(), pthread_barrier_wait(). You write these calls directly into your control flow, and the compiler has no special awareness of what they mean — they’re just function calls.
OpenMP directives, by contrast, are recognized by the compiler itself. This has two big consequences. First, if you compile without OpenMP support enabled (e.g., without -fopenmp on GCC/Clang), directives are simply ignored, and your code still compiles and runs — serially, but correctly. Second, because the compiler understands directives semantically, it can insert the right synchronization code (barriers, locks, atomic instructions) automatically, tuned for the target platform, rather than you hand-rolling it.
The Core Synchronization Directives
Let me go through the main ones systematically.
#pragma omp barrier
Forces every thread in the current team to wait until all have reached this point.
#pragma omp parallel
{
phase1();
#pragma omp barrier
phase2(); // guaranteed phase1() finished on all threads first
}
#pragma omp critical [(name)]
Ensures mutual exclusion — only one thread executes the enclosed block at a time.
#pragma omp critical(update_total)
{
total += local_value;
}
#pragma omp atomic
A lightweight, hardware-backed form of mutual exclusion limited to simple expressions.
#pragma omp atomic
counter++;
#pragma omp single [nowait]
Restricts a block to execute on exactly one (arbitrary) thread of the team, while the others wait at an implicit barrier (unless nowait is specified).
#pragma omp parallel
{
#pragma omp single
{
setup_shared_resource(); // done once, by whichever thread gets there first
}
use_shared_resource(); // all threads wait for single to finish first
}
#pragma omp master / #pragma omp masked
Restricts a block to execute specifically on the primary (master) thread, unlike single, which allows any thread. Notably, master/masked has no implicit barrier — other threads skip past it immediately without waiting.
#pragma omp parallel
{
#pragma omp master
{
print_progress(); // only thread 0 does this, others don't wait
}
do_work();
}
(masked is the OpenMP 5.1+ replacement for master, with the added flexibility of specifying a filter thread via filter().)
#pragma omp ordered
Within a parallel loop, forces a specific block to execute in the same sequential order as the original loop iterations, even though the rest of the loop body runs in parallel. This requires the loop to have an ordered clause on its for directive.
#pragma omp for ordered
for (int i = 0; i < N; i++) {
compute(i); // runs in parallel, any order
#pragma omp ordered
{
printf("%d\n", i); // prints strictly in order: 0, 1, 2, 3...
}
}
This is genuinely useful when you need parallel computation but sequential output or side effects (like writing to a file in order).
#pragma omp taskwait and #pragma omp taskgroup
Synchronize on dynamically created tasks. taskwait blocks until direct child tasks finish; taskgroup blocks until all descendant tasks (children, grandchildren, etc.) within its scope finish.
#pragma omp taskgroup
{
#pragma omp task
{ level1();
#pragma omp task
{ level2(); }
}
} // waits for BOTH level1 and level2 tasks to complete
#pragma omp flush
Ensures a consistent, up-to-date view of shared variables across threads by forcing a memory synchronization point — the OpenMP equivalent of a memory fence. Many other directives (barrier, critical, atomic) include an implicit flush, so explicit flush is relatively rare in modern code, but it matters when you’re implementing custom synchronization protocols (like a manual spin-wait on a flag variable).
int flag = 0;
#pragma omp parallel sections
{
#pragma omp section
{
produce_data();
#pragma omp flush
flag = 1;
#pragma omp flush(flag)
}
#pragma omp section
{
while (!flag) {
#pragma omp flush(flag)
}
consume_data();
}
}
Diagram: Directive Categories
OpenMP Synchronization Directives
|
-------------------------------------------------
| | | |
Blocking/Barrier Mutual Exclusion Ordering Memory Consistency
- barrier - critical - ordered - flush
- single (implicit)- atomic
- taskwait - omp_lock_t API
- taskgroup
How the Compiler Translates Directives
When you compile OpenMP-annotated code with -fopenmp (GCC/Clang) or /openmp (MSVC), the compiler performs outlining: it extracts the body of each parallel region into a separate function, inserts calls to the OpenMP runtime library (libgomp, LLVM’s libomp, or Intel’s runtime) to manage thread creation, work distribution, and synchronization, and replaces directives like barrier and critical with actual runtime calls (GOMP_barrier(), lock acquire/release functions, etc.). This is why directives “disappear” cleanly when OpenMP is disabled — they’re purely compiler-recognized annotations, not runtime-dependent function calls that would break compilation on their own.
Real-World Use Cases
- Numerical solvers (finite element analysis, computational fluid dynamics) on Linux HPC systems use barriers and reductions extensively between solver iterations.
- Video encoding pipelines use
orderedto guarantee frames are written to the output stream in the correct sequence even though encoding itself is parallelized. - Windows-based CAD and simulation software commonly uses
singlefor one-time shared resource initialization inside a parallel region (like allocating a shared buffer) before all threads proceed to use it.
Troubleshooting
- Silent serialization — overuse of
criticalormaster(with unintended waiting patterns) can quietly kill your speedup; profile actual parallel efficiency, don’t assume directives are free. - Incorrect ordering assumptions — forgetting that
singlepicks an arbitrary thread (not necessarily the master) can break code that assumes thread 0 always executes it; usemaster/maskedif you specifically need the primary thread. - Stale shared data — on relaxed memory models, forgetting a
flushwhen hand-rolling synchronization (outside of the built-in directives, which already include implicit flushes) can lead to threads seeing outdated values of shared variables.
Best Practices
- Reach for built-in directives (
critical,atomic,barrier,reduction) before you consider manualflush-based spin-waiting — they’re safer and usually just as fast. - Use
nowaitdeliberately wherever you can prove there’s no cross-thread dependency, to reduce unnecessary barrier overhead. - Prefer
orderedonly for the specific block that truly needs sequential order, not the whole loop body, to preserve as much parallelism as possible. - Use named critical sections and separate lock objects to avoid unrelated code paths blocking each other unnecessarily.
Summary
OpenMP’s synchronization model is expressed entirely through compiler directives — barrier, critical, atomic, single, master/masked, ordered, taskwait, taskgroup, and flush — each targeting a different synchronization need, from full-team barriers to fine-grained mutual exclusion to strict ordering guarantees. Because these are compiler-recognized pragmas rather than plain function calls, the compiler can outline parallel regions and insert the appropriate runtime synchronization calls automatically, giving you a much higher-level, safer way to reason about concurrent execution than manually managing locks and condition variables.
FAQs
Q: What’s the difference between single and master? single lets any one thread execute the block (with an implicit barrier for the others by default); master (or masked) specifically restricts execution to the primary thread and has no implicit barrier.
Q: Do OpenMP directives work if I forget to enable OpenMP at compile time? Yes — they’re silently ignored, and the code runs correctly but serially, since directives are just structured comments/pragmas without the flag.
Q: When do I need an explicit flush? Rarely — mostly when implementing custom synchronization protocols outside the standard directives, since most directives already include an implicit flush.
Q: Does ordered make my whole loop sequential? No — only the code inside the ordered block executes in sequential order; the rest of the loop body still runs in parallel.
References
- OpenMP Architecture Review Board, OpenMP Application Programming Interface, Version 5.2, openmp.org.
- Chapman, B., Jost, G., Van der Pas, R. Using OpenMP, MIT Press.
- GNU libgomp manual, gcc.gnu.org/onlinedocs/libgomp
- LLVM OpenMP documentation, openmp.llvm.org
