Atomic

Files CCore/inc/task/Atomic.h CCore/src/task/Atomic.cpp

Atomic is an unsigned integral variable with a set of "atomic" operations. Such variable can be used safely by multiple threads without using mutexes. Moreover, efficient mutex implementation is based on atomics. Atomic operations (except reading a value) implies a "memory fence". This feature is irrelevant on single core systems, but for multi-core CPU it is essential to ensure the memory visibility coherence.


class Atomic : NoCopy
 {
   Sys::Atomic atomic;
   
  public: 
  
   using Type = Sys::Atomic::Type ;
   
   Atomic();
   
   using PrintProxyType = Type ;
  
   operator Type() const;
  
   // return previous value, memory fence is used

   Type operator  = (Type value);
  
   Type operator += (Type value);
  
   Type operator -= (Type value);
  
   Type operator ++ (int);
  
   Type operator -- (int);
  
   Type trySet(Type old_value,Type new_value);
 };

Atomic::Type is the underlying integral type. Usually it is a machine word type. This type is a PrintProxyType.

Default constructor sets the value to zero.

Implicit cast operator reads the value atomically with respect to other operations, but does not imply a memory fence.

All modifying operations imply a memory fence and return the value of the object before the operation. "Memory fence" means that if a thread red the modified value of the atomic, it sees all variable modifications, made before the atomic operation has been performed by the thread, who has done the atomic operation. On a single core CPU this is always true, because threads are not executed simultaneously and only a compiler optimization may be an issue. But on multi-core CPU it is possible, that concurrent execution of CPU read/write commands changes the order of visible variable modifications. So special CPU commands must be used to prevent this. An example:


volatile int a=0;
volatile int b=0;

// thread 1

b=1;
a=1;

// thread 2

while( a==0 );

int c=b; // c may be zero

operator = assigns a new value.

operator += increases the current value by the argument.

operator −= decreases the current value by the argument.

postfix operator ++ increments the current value.

postfix operator −− decrements the current value.

trySet() is a more complex operation. It compares the current value with the old_value. If they are equal, then it assigns the new_value. Otherwise it does nothing. You may learn what case has happened by comparing the return value with the old_value.

Sys::Atomic

Atomic implementation is based on the target atomic class Sys::Atomic, declared in the header sys/SysAtomic.h.


struct Sys::Atomic // POD type
 {
  // public
  
  using Type = unsigned ; // unsigned integral type, most likely unsigned

  // private data
  
  ....
  
  // public
  
  using PrintProxyType = Type ;
  
  void set_null();
  
  operator Type() const;
  
  // return previous value, memory fence is used
  
  Type operator  = (Type value);
  
  Type operator += (Type value);
  
  Type operator -= (Type value);
  
  Type operator ++ (int);
  
  Type operator -- (int);
  
  Type trySet(Type old_value,Type new_value);
 };

A typical implementation would be:


struct Sys::Atomic
 {
  // public
  
  using Type = ??? ; // unsigned integral type, most likely unsigned

  // private data
  
  volatile Type atomic; // volatile for the explicit Get()
  
  // private
  
  static Type Get(const volatile Type *atomic) { return *atomic; }
 
  static Type Set(volatile Type *atomic,Type value) noexcept;

  static Type Add(volatile Type *atomic,Type value) noexcept;

  static Type Sub(volatile Type *atomic,Type value) { return Add(atomic,-value); }
 
  static Type Inc(volatile Type *atomic) { return Add(atomic,1); }
 
  static Type Dec(volatile Type *atomic) { return Sub(atomic,1); }
 
  static Type TrySet(volatile Type *atomic,Type old_value,Type new_value) noexcept;
  
  // public
  
  using PrintProxyType = Type ;
  
  void set_null() { atomic=0; }
  
  operator Type() const { return Get(&atomic); }
  
  // return previous value, memory fence is used
  
  Type operator  = (Type value) { return Set(&atomic,value); }
  
  Type operator += (Type value) { return Add(&atomic,value); }
  
  Type operator -= (Type value) { return Sub(&atomic,value); }
  
  Type operator ++ (int) { return Inc(&atomic); }
  
  Type operator -- (int) { return Dec(&atomic); }
  
  Type trySet(Type old_value,Type new_value) { return TrySet(&atomic,old_value,new_value); }
 };

Only three highlighted functions must be implemented, usually using an assembler to prevent compiler optimizations across its calls.