Wednesday, February 1, 2023

More Interlocked problems.

 There are helpers like:

template <typename T>
bool TryCompareExchange(T volatile * p, T exchange, T* compare)
{
    T old = Traits<T>::InterlockedCompareExchange(p, exchange, *compare);
    bool success = (old == *compare);
    if (!success) *compare = old;
    return success;
}

Traits handles char, short, int, long __int64, unsigned, etc., and same-sized structs (hopefully with consistent/zeroed padding).

Leading to uses like:

void increment(long volatile * p)
{
    long old = *a;
    while (!TryCompareExchange(p, old + 1, &old)) ;
}

The problem comes when used with InterlockedCompareExchange128.
There is no atomic get/set128.
They can be synthesized from InterlockedCompareExchange128.

There are at least two incorrect forms, which do work in non-race conditions, so lull us into complacency ("it works"):

template <typename T>
T Get(T volatile * p)
{
    T value;
    while (!TryCompareExchange(p, *p, &value)) ; // incorrect code
    return value;
}

*p is a non-atomic read.
If p happens to match input value, then the non-atomic *p will be written back.
This can be mitigated by initializing value to a never-set value.
But finding such a value is not really needed (keep reading).


template <typename T>
void Set(T volatile * p, T value)
{
    TryCompareExchange(&value, value, p); // incorrect code
}

The write through the last parameter is not atomic.


The correct forms:

template <typename T>
T Get(T volatile * p)
{
    T value;
    (void)TryCompareExchange(p, value, &value);
    return value;
}

If the compare does match, the same value will be written back, atomically.

 

template <typename T>
void Set(T volatile * p, T value)
{
    T old = *p; // non-atomic read, ok
    while (!TryCompareExchange(p, value, &old)) ; 
}

Loop until the non-atomic read matches, upon which write the desired value atomically.