Back to C++
2026-03-025 min read

Free functions for atomic flags (C++)

Learn Free functions for atomic flags (C++) step by step with clear examples and exercises.

Why This Matters

Atomic flags play a crucial role in ensuring the safety of shared data access across multiple threads in C++ programs. By providing thread-safe operations on boolean values, they help avoid race conditions and other concurrency issues. In this lesson, we will delve deeper into the free functions provided by C++ for managing atomic flags, focusing on practical applications and real-world examples.

Prerequisites

To fully grasp this lesson, you should have a solid understanding of:

  1. The basics of C++ programming language
  2. Threads and concurrency issues in multi-threaded programs
  3. Familiarity with the Standard Template Library (STL) and its containers
  4. Knowledge of mutexes, condition variables, and other synchronization primitives
  5. Understanding of shared memory and its potential issues when accessed by multiple threads simultaneously

Core Concept

The C++ Standard Library offers several free functions for managing atomic flags within a program. These functions are defined in the `` header file and provide thread-safe operations on boolean values.

Atomic Flag Class

The std::atomic_flag class represents an atomic flag, which can be set or cleared atomically across multiple threads. It has two member functions: test_and_set() and test_and_clear().

#include <atomic>
#include <iostream>
#include <thread>

std::atomic_flag flag;

void setFlag(int id) {
flag.test_and_set();
std::cout << "Thread ID: " << id << ", setting flag\n";
}

void clearFlag(int id) {
while (flag.test_and_clear()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "Thread ID: " << id << ", trying to clear flag\n";
}
}

int main() {
std::vector<std::thread> threads;

for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(setFlag, i));
}

for (int i = 5; i < 10; ++i) {
threads.push_back(clearFlag, i);
}

for (auto& thread : threads) {
thread.join();
}

return 0;
}

In the above example, we create a shared atomic flag and define two functions: setFlag() and clearFlag(). The setFlag() function atomically sets the flag, while the clearFlag() function continuously tries to clear the flag until it is successfully cleared.

Atomic Flag Functions

In addition to the std::atomic_flag class, C++ provides several free functions for managing atomic flags:

  1. std::atomic_flag_test_and_set(): atomically tests and sets an atomic flag
  2. std::atomic_flag_test_and_clear(): atomically tests and clears an atomic flag
  3. std::atomic_flag_wait(): waits for an atomic flag to be set
  4. std::atomic_flag_notify_one(): notifies one waiting thread when an atomic flag is set
  5. std::atomic_flag_notify_all(): notifies all waiting threads when an atomic flag is set

These functions are useful for coordinating multiple threads and avoiding race conditions.

Worked Example

In this example, we'll create a producer-consumer scenario using atomic flags to ensure safe access to shared data.

#include <atomic>
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>

std::atomic_flag producerFlag;
std::atomic_flag consumerFlag;
std::mutex dataMutex;
std::queue<int> dataQueue;

void producer(int id) {
for (int i = 0; i < 10; ++i) {
producerFlag.test_and_set();
std::unique_lock<std::mutex> lock(dataMutex);
dataQueue.push(i * id);
consumerFlag.notify_one();
lock.unlock();
}
}

void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(dataMutex);
consumerFlag.wait(lock, []() { return !dataQueue.empty(); });
int data = dataQueue.front();
dataQueue.pop();
std::cout << "Thread ID: " << id << ", consumed data: " << data << '\n';
}
}

int main() {
std::vector<std::thread> producers;
std::vector<std::thread> consumers;

for (int i = 0; i < 2; ++i) {
producers.push_back(std::thread(producer, i));
}

for (int i = 2; i < 4; ++i) {
consumers.push_back(consumer, i);
}

for (auto& producer : producers) {
producer.join();
}

for (auto& consumer : consumers) {
consumer.join();
}

return 0;
}

In this example, we have two producers and two consumers that share a queue of data. The atomic flags producerFlag and consumerFlag are used to coordinate the producers and consumers, ensuring that they don't access the shared data simultaneously.

Common Mistakes

  1. Not initializing atomic flags: Always initialize atomic flags before using them in your program. For example: std::atomic_flag flag = ATOMIC_FLAG_INIT;
  2. Misusing atomic flags for non-boolean values: Atomic flags are designed to handle boolean values only. If you need to manage other types of shared data, consider using the std::atomic class instead.
  3. Not properly coordinating threads with atomic flags: Remember that setting an atomic flag does not automatically wake up any waiting threads. You must use notify_one() or notify_all() as needed.
  4. Using atomic flags inappropriately for synchronization: Atomic flags are useful for simple synchronization scenarios, but more complex concurrency issues may require other synchronization primitives like mutexes and condition variables.
  5. Not checking the return value of atomic flag functions: Always check the return values of atomic flag functions to ensure that they were successful. For example: if (!flag.test_and_set()) { /* handle error */ }
  6. Ignoring race conditions when using atomic flags: Be aware that atomic flags can still introduce race conditions if not used correctly or in combination with other synchronization primitives.

Practice Questions

  1. Write a program that uses atomic flags to implement a simple reader-writer scenario with two readers and one writer.
  2. Modify the producer-consumer example to handle cases where producers produce data faster than consumers can consume it.
  3. Implement a mutual exclusion lock using atomic flags in C++.
  4. Discuss the trade-offs between using atomic flags and other synchronization primitives like mutexes and condition variables.
  5. Explain how to use atomic flags for implementing spin locks in C++.

FAQ

  1. Why use atomic flags instead of mutexes for simple synchronization? Atomic flags offer better performance for simple synchronization scenarios, as they require less overhead compared to mutexes. However, for more complex scenarios, mutexes and condition variables may be more appropriate.
  2. Can I use atomic flags for shared data other than booleans? No, atomic flags are designed to handle boolean values only. For managing other types of shared data, consider using the std::atomic class instead.
  3. What is the difference between notify_one() and notify_all()? notify_one() wakes up one waiting thread, while notify_all() wakes up all waiting threads. Use notify_one() when you want to coordinate specific threads, and use notify_all() when you want to ensure that all waiting threads are notified.
  4. What is the overhead of using atomic flags compared to mutexes? Atomic flags have lower overhead than mutexes in simple synchronization scenarios due to their minimal locking mechanism. However, as the complexity of the synchronization increases, the overhead difference between atomic flags and mutexes becomes negligible.
  5. Are there any performance differences between test_and_set() and test_and_clear()? Both functions have similar performance characteristics, but test_and_set() is slightly faster than test_and_clear() because it sets the flag to true, while test_and_clear() sets the flag to false.
  6. Can I use atomic flags for implementing spin locks in C++? Yes, you can implement spin locks using atomic flags by repeatedly testing and setting the flag until it is clear (for a spin lock) or set (for a spin wait). However, be aware that excessive spinning can lead to high CPU usage and should be used judiciously.
Free functions for atomic flags (C++) | C++ | XQA Learn