Atomic Operations
Most .NET programming languages provide primitive atomic operations to work with fields with concurrent access. For example, C# volatile keyword is a language feature for atomic read/write of the marked field. But what if more complex atomic operation is required? Java provides such features at library level, with some overhead associated with object allocation. C# and many other .NET languages support concept of passing by refence so it is possible to obtain a reference to the field value. This ability allows to avoid overhead of atomic primitives typical to JVM languages. Moreover, extension methods may accept this parameter by reference forming the foundation for atomic operations provided by .NEXT library.
The library provides advanced atomic operations for the following types:
Numeric types have the following atomic operations:
- AccumulateAndGet, GetAndAccumulate - atomic modification of the field where modification logic is based on the supplied value and custom accumulator binary function
- UpdateAndGet, GetAndUpdate - atomic modification of the field where modification logic is based in the custom unary function
Reference types have similar set of atomic operations except arithmetic operations such as increment, decrement and addition.
Atomic operations for scalar types
Atomic operations are extension methods exposed by AtomicInt32 class.
Atomic operations for some data types represented by atomic containers instread of extension methods:
- Atomic.Boolean for bool data type
The following example demonstrates how to use advanced atomic operations
using DotNext.Threading;
public class TestClass
{
private long field;
public void IncByTwo() => field.UpdateAndGet(x => x + 2); //update field with a sum of its value and constant 2 atomically
public long Sub(long value) => field.AccumulateAndGet(value, (current, v) => current - value); //the same as field -= value but performed atomically
}
Atomic access for arbitrary value types
Volatile memory access is hardware dependent feature. For instance, on x86 atomic read/write can be guaranteed for 32-bit data types only. On x86_64, this guarantee is extended to 64-bit data type. What if you need to have hardware-independent atomic read/write for arbitrary value type? The naive solution is to use Synchronized method. It can be declared in class only, not in value type. If your volatile field declared in value type then you cannot use such kind of methods or you need to create container in the form of the class which requires allocation on the heap.
Atomic<T> is a container that provides atomic operations for arbitrary value type. The container is value type itself and do not require heap allocation. Memory access to the stored value is organized through software-emulated memory barrier which is portable across CPU architectures. Performance impact is very low. Under heavy lock contention, the access time is ~20-30% faster than Synchronized methods. Check Benchmarks for information.
The following example demonstrates how to organize atomic access to field of type Guid.
using DotNext.Threading;
class MyClass
{
private Atomic<Guid> id;
public void GenerateNewId() => id.Write(Guid.NewGuid()); //Write is atomic
public bool IsEmptyId
{
get
{
id.Read(out var value); //Read is atomic
return value == Guid.Empty;
}
}
}