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:
- VolatileRead
- VolatileWrite
- IncrementAndGet - atomic increment of the field
- DecrementAndGet - atomic decrement of the field
- CompareAndSet - atomic modification of the field based on comparison
- Add - atomic arithemtic addition
- GetAndSet, SetAndGet - atomic modification of the field with ability to obtain modified value as a result
- 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 grouped by specific target scalar types:
- AtomicInt32 for int
- AtomicUInt32 for int
- AtomicInt64 for long
- AtomicUInt64 for long
- AtomicSingle for float
- AtomicDouble for double
- AtomicIntPtr for IntPtr
- AtomicReference for reference types
Atomic operations for some data types represented by atomic containers instread of extension methods:
- AtomicBoolean for bool data type
- AtomicEnum<TEnum> for enum data types
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 void IncByTwo2() => field.Add(2); //the same effect
public long Sub(long value) => field.AccumulateAndGet(value, (current, v) => current - value); //the same as field -= value but performed atomically
}
Atomic operations for arrays
C# doesn't provide volatile access to array elements syntactically in contrast with volatile fields. .NEXT library provides the same set of atomic operations as for scalar types with a small difference: array atomic operation accept element index as additional argument.
The second approach utilizes extension method.
using DotNext.Threading;
var array = new double[10];
var result = array.IncrementAndGet(2); //2 is an index of array element to be modified
result = array.VolatileRead(2); //atomic read of array element
array.VolatileWrite(2, 30D); //atomic modification of array element
Atomic operations with pointers
Working with unmanaged memory in multithreaded application also requires atomic operations and volatile memory access. AtomicPointer provides all necessary functionality as extension methods for Pointer<T> data type.
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;
}
}
}