Multithreading program stuck in optimized mode but runs normally in -O0
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.
Introduction
When a multithreaded program works at -O0 but hangs or misbehaves with optimization enabled, the bug is usually already present in the code. The optimizer is not "breaking" a correct program. It is exposing a race condition, missing synchronization, or undefined behavior that -O0 happened to mask. The right fix is almost never "compile without optimization forever." It is to make the shared-state rules explicit.
Why -O0 Can Hide Bugs
At -O0, compilers preserve a lot of the original structure of the code. Reads and writes often happen in a way that feels close to the source. Once optimization is enabled, the compiler is free to reorder, cache, inline, and eliminate operations as long as the single-threaded abstract machine rules are preserved.
If your code has a data race, those transformations can make the bug visible.
Example of broken shared state:
This code has a data race on ready. Under optimization, the worker may never observe the write in a defined way.
Use Atomics or Locks for Shared Data
The correct fix is to synchronize access to shared state. For a simple flag, std::atomic<bool> is often enough.
Now the communication between threads is defined. The behavior no longer depends on optimization level luck.
Busy-Wait Loops Need Special Care
Programs that appear "stuck" under optimization often contain spin loops, hand-rolled flags, or shared variables accessed without synchronization. The compiler may keep values in registers, and CPUs may reorder memory operations. Without atomics, there is no guarantee another thread’s write becomes visible the way you expect.
That is why volatile is not a substitute for synchronization in normal multithreaded C or C++ code. Volatile affects certain compiler behaviors for special memory, but it does not provide the cross-thread ordering guarantees that atomics and mutexes do.
Prefer Higher-Level Synchronization
Many waiting problems are better expressed with condition variables than with manual spin loops.
This is clearer and far more robust than a custom polling loop.
Look for Other Undefined Behavior Too
Optimization-sensitive multithreading bugs are not always just missing atomics. Other undefined behavior can become visible only in optimized builds, including:
- accessing objects after lifetime ends
- unsafely sharing non-thread-safe containers
- reading partially initialized memory
- relying on timing rather than synchronization
A program that "only breaks under -O2" is usually already broken. Optimization is just making the invalid assumptions harder to ignore.
Debug with Sanitizers and Reduced Test Cases
The most effective debugging step is usually to shrink the failing code path and run a thread sanitizer build if the toolchain supports it. Sanitizers are far more informative than staring at optimized assembly and hoping the bug explains itself.
Even without specialized tooling, turning a suspected pattern into a tiny reproducible example often reveals whether the problem is a missing atomic, a lock inversion, or a lifetime issue.
Common Pitfalls
- Blaming the optimizer instead of the underlying race or undefined behavior.
- Using plain shared booleans or integers for thread communication.
- Treating volatile as a complete multithreading fix.
- Building custom spin loops when a mutex or condition variable would express the intent better.
- Assuming code is thread-safe because it "usually works" at
-O0.
Summary
- If optimized multithreaded code hangs while
-O0works, the code likely already contains a synchronization bug. - Compilers and CPUs are allowed to reorder unsynchronized operations in ways that expose data races.
- Use
std::atomic, mutexes, and condition variables for cross-thread communication. - Volatile is not a replacement for proper synchronization.
- Fix the race rather than relying on low optimization to hide it.
Related reading
- My numpy build doesn't use multiple CPU cores
- MySQL - force not to use cache for testing speed of query
- mysql - how many columns is too many?
- MySQL - length vs char_length
- Multithreading What is the point of more threads than cores?
- Multithreading with Jersey
- Must call EndRead in ALL cases?
- Mutating Webhook does not invoke endpoint because certificate signed by unknown authority

DSA Fundamentals
Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Data Structures & Algorithms practice on Codemia
Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.