SoFunction
Updated on 2025-03-02

Detailed explanation of C++11 atomic types and atomic operations

1. Understand atomic operations

Atomic operations are "small and non-parallel" operations in multi-threaded programs, meaning that when multiple threads access the same resource, only one thread can operate on the resource. Generally speaking, atomic operations can be guaranteed through mutually exclusive access methods, such as mutexes under Linux, critical section under Windows, etc. Here is a Linux environment that uses the POSIX standard pthread library to implement atomic operations under multithreading:

#include <>
#include <iostream>
using namespace std;

int64_t total=0;
pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;

//Thread function, used for accumulationvoid* threadFunc(void* args)
{
  int64_t endNum=*(int64_t*)args;
  for(int64_t i=1;i<=endNum;++i)
  {
    pthread_mutex_lock(&m);
    total+=i;
    pthread_mutex_unlock(&m);
  }
}

int main()
{
  int64_t endNum=100;
  pthread_t thread1ID=0,thread2ID=0;
  
  //Create thread 1  pthread_create(&thread1ID,NULL,threadFunc,&endNum);
  //Create thread 2  pthread_create(&thread2ID,NULL,threadFunc,&endNum);
  
  //Blocking and waiting for thread 1 to end and recycle resources  pthread_join(thread1ID,NULL);
  //Blocking and waiting for thread 2 to end and recycle resources  pthread_join(thread2ID,NULL);

  cout<<"total="<<total<<endl;	//10100
}

In the above code, two threads operate on total at the same time, in order to ensuretotal+=i Atomicity, a mutex lock is used to ensure that only the same thread executes at the same timetotal+=iOperation, so get the correct resulttotal=10100. If there is no mutual exclusion process, then the total may be operated by two threads at the same time at the same time, that is, two threads will read the total value in the register at the same time, and then write it to the register after operation. In this way, the increase operation of one thread will be invalid, and a random error value less than 10100 will be obtained.

++11 implements atomic operation

Before C++11, parallel programming could be implemented using third-party APIs, such as pthread multi-thread library, but when using it, it is necessary to create mutex locks and perform operations such as locking and unlocking to ensure that multi-threading atomic operations on critical resources, which undoubtedly increases the development workload. However, starting from C++11, C++ has supported parallel programming from the language level, including managing threads, protecting shared data, synchronous operations between threads, low-level atomic operations, etc. The new standard greatly improves the portability of programs. In the past, multi-threading relies on specific platforms, but now there is a unified interface.

C++11 helps developers easily implement atomic operations by introducing atomic types.

#include <atomic>
#include <thread>
#include <iostream>
using namespace std;

atomic_int64_t total = 0;  //atomic_int64_t is equivalent to int64_t, but it has atomicity itself
//Thread function, used for accumulationvoid threadFunc(int64_t endNum)
{
	for (int64_t i = 1; i <= endNum; ++i)
	{
		total += i;
	}
}

int main()
{
	int64_t endNum = 100;
	thread t1(threadFunc, endNum);
	thread t2(threadFunc, endNum);

	();
	();

	cout << "total=" << total << endl; //10100
}

The program compiles normally and runs to output the correct resultstotal=10100. Using the atomic type provided by C++11 and the multi-threaded standard interface, the atomic operation of multi-threaded critical resources is simply implemented. Atomic type C++11 passesatomic<T>Class templates are defined, for example, atomic_int64_t is through typedefatomic<int64_t> atomic_int64_tImplemented, the header file must be included when using it<atomic>. In addition to providing atomic_int64_t, other atomic types are also provided. Common atomic types are

Atomic type name

Corresponding to built-in type

atomic_bool

bool

atomic_char

atomic_char

atomic_char

signed char

atomic_uchar

unsigned char

atomic_short

short

atomic_ushort

unsigned short

atomic_int

int

atomic_uint

unsigned int

atomic_long

long

atomic_ulong

unsigned long

atomic_llong

long long

atomic_ullong

unsigned long long

atomic_ullong

unsigned long long

atomic_char16_t

char16_t

atomic_char32_t

char32_t

atomic_wchar_t

wchar_t

Atomic operations are platform-related. Atomic types can implement atomic operations because C++11 abstracts the operations of atomic types, defines a unified interface, and requires the compiler to generate specific implementations of platform-related atomic operations. The C++11 standard defines atomic operations as member functions of the atomic template class, including read (load), write (store), exchange (exchange), etc. For built-in types, it is mainly done by overloading some global operators. For example, the atomic addition operation of total+=i above is achieved by overloading operator+=. If you compile with g++, on x86_64 machines, the operator+=() function will generate a special lock-prefixed x86_64 instruction to control the bus and implement atomic addition on the x86_64 platform.

There is a special atomic type that is atomic_flag, because atomic_flag is different from other atomic types. It is lock-free, that is, threads do not need to lock it to access it, and other atomic types are not necessarily lock-free. Because atomic<T> cannot guarantee that type T is lock-free, and the processor processing methods of different platforms are different, it cannot guarantee that there must be lock-free, so other types will have is_lock_free() member function to determine whether it is lock-free. atomic_flag only supports two member functions: test_and_set() and clear(). The test_and_set() function checks the std::atomic_flag flag. If std::atomic_flag has not been set before, set the flag of std::atomic_flag; if std::atomic_flag has been set before, return true, otherwise return false. The clear() function clears the std::atomic_flag flag so that the next call to std::atomic_flag::test_and_set() returns false. A spin lock can be implemented using atomic_flag member functions test_and_set() and clear():

#include &lt;&gt;
#include &lt;atomic&gt;
#include &lt;thread&gt;
#include &lt;iostream&gt;

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void func1()
{
	while (lock.test_and_set(std::memory_order_acquire)) // Set to true in the main thread, you need to wait for the t2 thread to clear {
  std::cout &lt;&lt; "func1 wait" &lt;&lt; std::endl;
 }
 std::cout &lt;&lt; "func1 do something" &lt;&lt; std::endl;
}

void func2()
{
 std::cout &lt;&lt; "func2 start" &lt;&lt; std::endl;
 ();
}

int main()
{
 lock.test_and_set();    // Set status std::thread t1(func1);
 usleep(1);					 	//Sleep 1us std::thread t2(func2);

 ();
 ();

 return 0;
}

In the above code, an atomic_flag object lock is defined, and initialized with the initial value ATOMIC_FLAG_INIT, that is, it is in a false state. Thread t1 calls test_and_set() and returns true (because it has been set in the main thread), so it is waiting. After waiting for a while, when thread t2 runs and calls clear(), test_and_set() returns false and exits the loop and waits for the corresponding operation. In this way, one thread is achieved to wait for another thread. Of course, it can be encapsulated into a lock operation method, such as:

void Lock(atomic_flag& lock){ while ( lock.test_and_set()); }
void UnLock(atomic_flag& lock){ (); }

In this way, the critical area can be accessed mutually exclusively through Lock() and UnLock() methods.

The above is a detailed explanation of C++11 atomic types and atomic operations. For more information about C++11 atomic types and atomic operations, please follow my other related articles!