This lesson gives an overview of the atomic flag, which is used from the perspective of concurrency in C++.

The atomic flag, i.e. std::atomic_flag, has a very simple interface. Its clear method enables you to set its value to false; with the test_and_set method you can set the value back to true. There is no method to exclusively ask for the current value. To use std::atomic_flag it must be initialized to false with the constant ATOMIC_FLAG_INITstd::atomic_flag has two outstanding properties.

std::atomic_flag is:

The only lock-free atomic? The remaining more powerful atomics can provide their functionality by using a mutex internally according to the C++ standard. These remaining atomics have a method called is_lock_free to check if the atomic uses a mutex internally. On the popular microprocessor architectures, I always get the answer true. That being said, my implementation internally uses no mutex; you should be aware of this and check it on your target system if you want to program lock-free.

The interface of std::atomic_flag is powerful enough to build a spinlock. With a spinlock, you can protect a critical section as you would with a mutex. The spinlock will not passively wait, in contrast to a mutex, until it gets it to lock. It will eagerly ask for the lock to get access to the critical section.it fully utilizes the CPU and does waste CPU cycles.

The example shows the implementation of a spinlock with the help of std::atomic_flag.

// spinLock.cpp
#include <iostream>
#include <atomic>
#include <thread>

class Spinlock{
  std::atomic_flag flag;
public:
  Spinlock(): flag(ATOMIC_FLAG_INIT){}
  
  void lock(){
    while( flag.test_and_set() );
  }

  void unlock(){
    flag.clear();
  }
};

Spinlock spin;

void workOnResource(){
  spin.lock();
  // shared resource
  spin.unlock();
  std::cout << "Work done" << std::endl;
}

int main(){
  std::thread t(workOnResource);
  std::thread t2(workOnResource);

  t.join();
  t2.join();

}

Both threads t and t2 (lines 31 and 32) are competing for the critical section. For simplicity, the critical section in line 24 consists only of a comment. How does it work? The class Spinlock has the methods lock and unlock- similar to a mutex. In addition to this, the constructor of Spinlock initializes the std::atomic_flag to false (line 9).

If thread t is going to execute the function workOnResource, the following scenarios can happen:

  1. Thread t gets the lock because the lock invocation was successful. The lock invocation is successful if the initial value of the flag in line 12 is false. In this case, thread t sets it in an atomic operation to true. The value true is the value the while loop returns to thread t2 if it tries to get the lock. So thread t2 is caught in the rat race. Thread t2 has no possibility to set the value of the flag to false, so t2 must wait until thread t executes the unlock method and sets the flag to false (lines 15 - 17).
  2. Thread t doesn’t get the lock, so we are in scenario 1 with swapped roles.

I want you to focus your attention on the method test_and_set of std::atomic_flag. The method test_and_set consists of two operations: reading and writing. It’s key that both operations are performed in one atomic operation. If not, we would have a read and a write on the shared resource (line 24). That is-- by definition-- a data race, and the program has undefined behavior.