Back to TILs

C++ atomic 01

Date: 2021-02-22Last modified: 2024-02-26

Table of contents

Atomic operations is an indivisible operation. We cannot observe such an operation half-done from any thread in the systesm.

Steps required for i++;

Image caption.
Fig. 1 - Image caption.

atomic_flag

Most basic type, it is like a bool.

Operations:

void demo_atomic_flag()
{
  fmt::print( "==== {} ====\n", __FUNCTION__ );
  std::atomic_flag flag1 = ATOMIC_FLAG_INIT;
  fmt::print( "#1 The previous value of flag1 is {}\n", flag1.test_and_set() ); // false
  fmt::print( "#2 The previous value of flag1 is {}\n", flag1.test_and_set() ); // true
  fmt::print( "#3 The previous value of flag1 is {}\n", flag1.test_and_set() ); // true
  flag1.clear();
  fmt::print( "#4 The previous value of flag1 is {}\n", flag1.test_and_set() ); // false
}

atomic_bool

void demo_atomic_bool()
{
  fmt::print( "==== {} ====\n", __FUNCTION__ );
  // clang false by default
  // msvs true by default
  std::atomic<bool> flag1;
  fmt::print( "The value of atomic<bool> flag1 is {}\n", flag1.load() ); // false
  fmt::print( "Is lock free: {}\n", flag1.is_lock_free() );

  // Compilation error std::atomic<bool> flag2(flag1);
  // Compilation error std::atomic<bool> flag3 = flag1;

  bool              non_atomic_bool{ true };
  std::atomic<bool> flag4( non_atomic_bool );
  fmt::print( "The value of atomic<bool> flag4 is {}\n", flag4.load() ); // true
  std::atomic<bool> flag5 = false;
  fmt::print( "The value of atomic<bool> flag5 is {}\n", flag5.load() ); // false

  flag5.store( true );
  fmt::print( "The value of atomic<bool> flag5 is {} after store\n", flag5.load() ); // true

  auto previous_value = flag5.exchange( false );
  fmt::print( "previous_value is {} and flag5 is {}\n", previous_value, flag5.load() ); // false
}

atomic_int

std::atomic<int> x{0};
++x;           // atomic pre-increment
x++;           // atomic post-incrment
x += 1;        // atomic increment
x |= 2;        // atomic bit set
int y = x * 2; // atomic read of x
x = y + 1;     // atomic write
x = x + 1;     // atomic read followed by atomic write
x = x * 2;     // atomic read followed by atomic write
---- x *= 2;   // DOES NOT EXIST ATOMIC MULTIPLICATION
Operation.
Fig. 2 - Operation.

Compare exchange

bool r = X.compare_exchange_weak( T& expected, T desired )
bool r = X.compare_exchange_strong( T& expected, T desired )

weak: if the CPU does not have a simple compare exchange instruction is not possible to garantee the atomicity of the operation. More performant.

strong: to garantee the atomicity this function may require more CPU instructions.

See Compare and swap.

This is the logic in the Intel Software Manual Vol 2A:

bool compare_and_swap( int *accum, int *dest, int newval )
{
  if( *accum == *dest ) {
    *dest = newval;
    return true;
  }
  else {
    *accum = *dest;
    return false;
  }
}
void demo_atomic_int()
{
  fmt::print( "==== {} ====\n", __FUNCTION__ );

  std::atomic<int> x1( 20 );
  int              expected_value_1 = 20;
  int              desired_value_1  = 6;

  fmt::print( "#1 value of x1 is {}\n", x1.load() );
  bool r1 = x1.compare_exchange_weak( expected_value_1, desired_value_1 );
  fmt::print( "#2 value of x1 is {}\n", x1.load() );
  fmt::print( "#3 value of desired_value_1 is {}\n", desired_value_1 );
  fmt::print( "#4 value of expected_value_1 is {}\n", expected_value_1 );
  fmt::print( "#5 is store executed? {}\n", r1 );

  std::atomic<int> x2( 10 );
  int              expected_value_2 = 20;
  int              desired_value_2  = 7;

  fmt::print( "#6 value of x2 is {}\n", x1.load() );
  bool r2 = x2.compare_exchange_weak( expected_value_2, desired_value_2 );
  fmt::print( "#7 value of x2 is {}\n", x2.load() );
  fmt::print( "#8 value of desired_value_2 is {}\n", desired_value_2 );
  fmt::print( "#9 value of expected_value_2 is {} (updated)\n", expected_value_2 );
  fmt::print( "#10 is store executed? {}\n", r2 );
}
atomic<unsigned long> sum1( 0 );

void doWork1( size_t N, unsigned long *a )
{
  for( size_t i = 0; i < N; ++i ) {
    sum1 += a[i];
  }
}
unsigned long sum2( 0 );
mutex         M;
void doWork2( size_t N, unsigned long *a )
{
  unsigned long s = 0;
  for( size_t i = 0; i < N; ++i ) {
    s += a[i];
  }
  lock_guard<mutex> L( M );
  sum2 += s;
}
int main()
{
  demo_atomic_flag();
  demo_atomic_bool();
  demo_atomic_int();

  // Size of array
  constexpr size_t        N = 10000;
  array<unsigned long, N> data;
  // Array initialization
  iota( data.begin(), data.end(), 1000 );

  thread t1( doWork1, N, data.data() );
  thread t2( doWork2, N, data.data() );

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

  cout << "Sum1: " << sum1 << endl;
  cout << "Sum2: " << sum2 << endl;
  return 0;
}

Possible output

==== demo_atomic_flag ====
#1 The previous value of flag1 is false
#2 The previous value of flag1 is true
#3 The previous value of flag1 is true
#4 The previous value of flag1 is false
==== demo_atomic_bool ====
The value of atomic<bool> flag1 is false
Is lock free: true
The value of atomic<bool> flag4 is true
The value of atomic<bool> flag5 is false
The value of atomic<bool> flag5 is true after store
previous_value is true and flag5 is false
==== demo_atomic_int ====
#1 value of x1 is 20
#2 value of x1 is 6
#3 value of desired_value_1 is 6
#4 value of expected_value_1 is 20
#5 is store executed? true
#6 value of x2 is 6
#7 value of x2 is 10
#8 value of desired_value_2 is 7
#9 value of expected_value_2 is 10 (updated)
#10 is store executed? false
Sum1: 59995000
Sum2: 59995000

References