Detail C++11 atomic types and atomic operations

  • 2020-11-18 06:22:52
  • OfStack

1. Understand atomic operations

Atomic operations are "minimal and non-parallelizable" operations in multithreaded programs, meaning that when multiple threads access the same resource, only one thread can operate on the resource. In general, atomic operations can be guaranteed by means of mutually exclusive access, such as the mutex under Linux (mutex), the critical section under Windows (Critical Section), etc. Here is an Linux environment using the POSIX standard pthread library to achieve multi-threaded atomic operations:


#include <pthread.h>
#include <iostream>
using namespace std;

int64_t total=0;
pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;

// Thread function, used for summing 
void* 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 a thread 1
  pthread_create(&thread1ID,NULL,threadFunc,&endNum);
  // Create a thread 2
  pthread_create(&thread2ID,NULL,threadFunc,&endNum);
  
  // Blocking waiting thread 1 End and recycle the resource 
  pthread_join(thread1ID,NULL);
  // Blocking waiting thread 2 End and recycle the resource 
  pthread_join(thread2ID,NULL);

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

In the above code, both threads operate on total at the same time, to ensure that total+=i The mutex is used to ensure that only the same thread executes at the same time total+=i Operation, so get the correct result total=10100 . If mutex is not done, then total may be operated by two threads at the same time, that is, two threads read the total value in the register at the same time, and then write to the register after operating separately. In this way, the increment operation of one thread will be invalid, and a random error value less than 10100 will be obtained.

2.C++11 realizes atomic operation

Before C++11, the third party API can be used to achieve parallel programming, such as pthread multi-threaded library, but the use of the mutex, lock, unlock and other operations to ensure the atomic operation of multithreaded critical resources, which undoubtedly increases the workload of development. However, starting with C++11, C++ supports parallel programming at the language level, including managing threads, protecting Shared data, synchronizing operations between threads, low-level atomic operations and other classes. The new standard greatly improves the portability of programs. Instead of relying on specific platforms, multithreading now has a unified interface.

C++11 helps developers easily manipulate atoms by introducing atomic types.


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

atomic_int64_t total = 0;  //atomic_int64_t The equivalent of int64_t But it has atomic properties 

// Thread function, used for summing 
void 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);

	t1.join();
	t2.join();

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

The program compiles and runs normally and outputs the correct results total=10100 . C++11 provides a standard interface between atomic type and multithreading, which concisely realizes the atomic operation of multithreading on critical resources. Atomic type C++11 passed atomic<T> Class templates are defined, such as atomic_int64_t via typedef atomic<int64_t> atomic_int64_t Implementation, the use of the need to include header files <atomic> . In addition to providing atomic_int64_t, other atomic types are provided. Common types of atoms are

原子类型名称

对应内置类型

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 operation is platform-dependent, and atomic type can realize atomic operation because C++11 abstracts the operation of atomic type, defines the interface of Unity 1, and requires the compiler to produce concrete implementation of platform-dependent atomic operation. The C++11 standard defines atomic operations as member functions of the atomic template class, including read (load), write (store), exchange (exchange), and so on. For built-in types, this is done primarily by overloading 1 of the global operators. For example, total+=i atom add operation above is achieved by overloading operator+=. Compiled using g++, the operator+=() function produces a special x86_64 instruction prefixed by lock to control the bus and implement atomic addition on the x86_64 platform.

A special atomic type is atomic_flag, because atomic_flag is unlocked (lock_free) unlike other atomic types, that is, threads do not need to lock their access, while other atomic types must be unlocked. Because atomic < T > There is no guarantee that type T is unlocked, and there is no guarantee that it will be unlocked depending on how the processor handles it on different platforms, so every other type will have a member function of is_lock_free() to determine if it is unlocked. atomic_flag only supports test_and_set() and clear(), test_and_set() checks the std::atomic_flag flag and sets the std:: atomic_flag flag if std::atomic_flag has not been set before. 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. One spin lock (spin lock) can be achieved using the member functions test_and_set() and clear() of atomic_flag:


#include <unistd.h>
#include <atomic>
#include <thread>
#include <iostream>

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void func1()
{
	while (lock.test_and_set(std::memory_order_acquire)) //  Set to in the main thread true Need to wait t2 thread clear
 {
  std::cout << "func1 wait" << std::endl;
 }
 std::cout << "func1 do something" << std::endl;
}

void func2()
{
 std::cout << "func2 start" << std::endl;
 lock.clear();
}

int main()
{
 lock.test_and_set();    //  Set the state of 
 std::thread t1(func1);
 usleep(1);					 	// sleep 1us
 std::thread t2(func2);

 t1.join();
 t2.join();

 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 the state of false. Thread t1 calls test_and_set()1 directly returns true (because it was set in the main thread), so 1 waits until after 1 period when thread t2 runs and calls clear(), test_and_set() returns false to exit the loop wait and do the corresponding operation. In this way, one thread waits for another thread. Of course, it can be encapsulated as a lock operation, such as:


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

This allows exclusive access to the critical section via Lock() and UnLock().

These are the details of C++11 atomic types and atomic operations. For more information on C++11 atomic types and atomic operations, please follow other related articles on this site!


Related articles: