Interlocked operations

When a shared data is small enough and you only need to increment it or swap two values, there's an option to do that without locking. All modern processors implement instructions that can do simple operations on memory locations in such a way that another processor can not interrupt the operation in progress.

Such instructions are sometimes called interlocked operations, while the whole idea of programming with them is called lock-free programming or, sometimes, microlocking. The latter term is actually more appropriate, as the CPU indeed does some locking. This locking happens on an assembler instruction level, and is therefore much faster than the operating system-based locking, such as critical sections.

As these operations are implemented on an assembler level, you have to adapt the code to the specific CPU target. Or, and this is very much advised, you just use the TInterlocked class from the System.SyncObjs unit.

Functions in the TInterlocked class are mostly just simple wrappers for the Atomic* family of the CPU-dependent function from the System unit. For example, TInterlocked.Add is implemented as a simple call to AtomicIncrement:

class function TInterlocked.Add(var Target: Integer; Increment: Integer): Integer;
begin
Result := AtomicIncrement(Target, Increment);
end;

Interlocked functions are declared as inline, so you won't lose any cycles if you call TInterlocked.Add instead of AtomicIncrement directly.

Windows implements more interlocked functions than are exposed in the TInterlocked class. If you want to call them directly, you can find them in the Winapi.Windows unit. All their names start with Interlocked.

We can split interlocked functions into two big families. One set of functions modifies a shared value. You can use them to increment or decrement a shared value by some amount. Typical representatives of this family are Increment, Decrement, and Add.

The other set of functions is designed to exchange two values. It contains only two functions with multiple overloads that support different types of data, Exchange and CompareExchange.

The functions from the first family are easy to use. They take a value, increment or decrement it by some amount, and return the new value. All that is done atomically, so any other thread in the system will always see the old value or the new value but not some intermediate value. 

The only exception to that pattern are the BitTestAndSet and BitTestAndClear functions. They both test if some bit in a value is set and return that as a boolean function result. After that they either set or clear the same bit, depending on the function. Of course, all that is again done atomically.

We can use TInterlocked.Increment and TInterlocked.Decrement to manipulate the shared value in the IncDec demo. Although TInterlocked is a class, it is composed purely from class functions. As such, we never create an instance of the TInterlocked class, but just use its class methods directly on our code, as shown in the following example:

procedure TfrmIncDec.InterlockedIncValue;
var
i: integer;
begin
for i := 1 to CNumRepeat do
TInterlocked.Increment(FValue);
end;

procedure TfrmIncDec.InterlockedDecValue;
var
i: integer;
begin
for i := 1 to CNumRepeat do
TInterlocked.Decrement(FValue);
end;

As we've seen in the previous section, this approach beats even the spinlocks, which were the fastest locking mechanism so far. 

Functions in the second family are a bit harder to use. Exchange takes two parameters: shared data and a new value. It returns the original value of the shared data and sets it to the new value.

In essence, the Exchange function implements the steps outlined in the following function, except that they are done in a safe, atomic way:

class function TInterlocked.Exchange(var Target: Integer; Value: Integer): Integer;
begin
Result := Target;
Target := Value;
end;

The second set of data exchanging functions, CompareExchange, are a bit more complicated. CompareExchange takes three parameters—shared data, new value, and test value. It compares shared data to the test value and, if the values are equal, sets the shared data to the new value. In all cases, the function returns the original value of the shared data.

he following code exactly represents the CompareExchange behavior, except that the real function implements all the steps as one atomic operation:

class function CompareExchange(var Target: Integer; Value: Integer; Comparand: Integer): Integer;
begin
Result := Target;
if Target = Comparand then
Target := Value;
end;

How can we know whether the shared data was replaced with the new value? We can check the function result. If it is equal to the test value, Comparand, then the value was replaced, otherwise it was not.

A helper function, TInterlocked.Read, can be used to atomically read from the shared data. It is implemented as such:

class function TInterlocked.Read(var Target: Int64): Int64;
begin
Result := CompareExchange(Target, 0, 0);
end;

If the original value is equal to 0, it will be replaced with 0. In other words, it will not be changed. If it is not equal to 0, it also won't be changed because CompareExchange guarantees that. In both cases, CompareExchange will return the original value, which is exactly what the function needs.

We can use TInterlocked.Read and TInterlocked.Exchange to re-implement the ReadWrite demo without locking:

procedure TfrmReadWrite.InterlockedReader;
var
i: integer;
value: int64;
begin
for i := 1 to CNumRepeat do begin
value := TInterlocked.Read(FPValue^);
FValueList.Add(value);
end;
end;

procedure TfrmReadWrite.InterlockedWriter;
var
i: integer;
begin
for i := 1 to CNumRepeat do begin
TInterlocked.Exchange(FPValue^, $7777777700000000);
TInterlocked.Exchange(FPValue^, $0000000077777777);
end;
end;

If you run the code, you'll see that it is indeed faster than the locking approach. On the test computer it executes about 20% faster, as you can verify in the following screenshot:

Similar to locking, interlocking is not a magic bullet. You have to use it correctly and you have to use it in all the relevant places. If you modify shared data with interlocked instructions but read that same data with a normal read, the code will not work correctly.

You can verify that by changing the value := TInterlocked.Read(FPValue^); line in the InterlockedReader method to value := FPValue^;. If you rerun the test, it will give the same (wrong) results as the original, fully unsafe version.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.229.86