Atomic flags in C++ are a type of atomic variable provided by the C++ Standard Library’s <atomic>
header. In programming, the term atomic refers to operations that are indivisible and immune to interference from other threads. These flags are designed to implement lock-free synchronization mechanisms, ensuring safe concurrent access to shared data without the risk of data races.
An atomic flag is a simple boolean variable that supports atomic access, meaning that concurrent operations on the flag do not produce data races. The operations on atomic flags are lock-free, eliminating the need for locks or other blocking mechanisms that can cause threads to wait.
The key operations available for atomic flags include:
test_and_set()
: This sets the atomic flag to true
and returns its previous value. This operation is atomic, ensuring that only one thread can successfully set the flag.clear()
: This sets the atomic flag to false
. This operation is also atomic and can be used to reset the flag.In the context of concurrent programming, atomic flags are valuable synchronization primitives. They enable threads to coordinate their activities without the need for explicit locks. For instance, atomic flags are commonly employed in implementing spin locks. In a spin lock, a thread repeatedly checks an atomic flag until it is set by another thread, indicating that a critical section is available for execution.
Here’s a simple example demonstrating the usage of atomic flags in C++:
#include <iostream>#include <atomic>#include <thread>std::atomic_flag flag = ATOMIC_FLAG_INIT;void worker(){while (flag.test_and_set(std::memory_order_acquire)){// Spin until the flag is cleared.}std::cout << "Critical section entered." << std::endl;// Perform critical section operations...std::cout << "Critical section exited." << std::endl;flag.clear(std::memory_order_release);}int main(){std::thread t1(worker);std::thread t2(worker);// Allow some time for the threads to start.std::this_thread::sleep_for(std::chrono::milliseconds(100));// Set the flag to allow one of the threads to enter the critical section.flag.clear(std::memory_order_release);t1.join();t2.join();return 0;}
Lines 1–3: The necessary headers for the code are included: <iostream>
for input/output operations, <atomic>
for atomic types and operations, and <thread>
for creating and managing threads.
Line 5: An atomic flag named flag
is declared and initialized with the macro ATOMIC_FLAG_INIT
. This macro ensures that the flag is initialized to a cleared state.
Lines 7–21: The worker
function represents the code executed by each worker thread. The function uses a spin-lock mechanism to wait until the flag is cleared. It repeatedly calls test_and_set()
on the flag with std::memory_order_acquire
memory order, ensuring atomicity and proper synchronization. Once the flag is cleared, it enters the critical section, performs some operations, and then clears the flag using flag.clear(std::memory_order_release)
.
Lines 25–26: Two threads, t1
and t2
, are created and assigned the worker
function to be executed concurrently.
Line 29: The main thread pauses for 100
milliseconds using std::this_thread::sleep_for()
. This allows some time for the worker threads to start and begin spinning on the flag.
Line 32: The main thread clears the flag using flag.clear(std::memory_order_release)
. This allows one of the worker threads to exit the spinning loop and enter the critical section.
Lines 34–35: The main thread waits for the worker threads, t1
and t2
, to complete their execution and join the main thread. This ensures the main thread doesn’t exit prematurely before the worker threads finish their work.
Note: Atomic flags provide a lightweight and efficient mechanism for synchronization in concurrent programming. Leveraging them judiciously is crucial for avoiding data races and other synchronization issues. Remember to ensure proper coordination and synchronization between threads when using atomic flags to achieve safe and predictable concurrent behavior.