What is thread safety anyway?

Multithreading can be hard to do right. The most common point of failure is assuming some code is thread safe when it actually is not. And then the whole multithreading castle crumbles into ruins.


"Thread safe" is a pretty vague term. If you are not sure what it actually means, I suggest you start by reading Eric Lippert's blog post on the subject. On the other hand, if you do know what thread safety is, well, I suggest you read it anyway ðŸ™‚

What is this thing you call "thread safe"?


Wrong assumptions



When it comes to thread safety in Delphi (actually, this is not a Delphi-specific thing) there is very little built in by default. Basically, if you are writing some code that has to be thread safe, you have to take care of the thread safety part all by yourself. And you have to be very careful with your assumptions, as you can easily come to the wrong conclusions.

To demonstrate how deeply unsafety goes, and how easy is to make wrong assumptions, I will use ARC as an example.

Of course, accessing the content of an object instance is not thread safe in terms of having multiple threads reading and writing to that content. But what about references? If you don't have to change the content of the object instance across multiple threads, then surely you can safely handle its references and its lifetime across thread boundaries without additional safety mechanisms - locks? After all, reference counting itself uses a locking mechanism to ensure a consistent reference count.

If you think ARC references are thread safe, think again. They are not thread safe at all. Not even close. Even something as trivial as the assignment of one reference to another can lead to disaster in a multithreaded scenario. To be fair, assignment of anything but the most basic simple types is not trivial at all.

The only thread safe part of the reference counting mechanism is keeping the reference count variable in a consistent state. That variable and that variable alone is protected from being simultaneously accessed from multiple threads during reference count increments or decrements. And there is more code involved in assigning one reference to another than in changing the reference count variable.

Assigning nil - clearing the reference - calls the _IntfClear or _InstClear helper functions, depending whether you are dealing with an interface reference under all compilers, or an object reference under ARC compilers. Assigning one reference to another calls the _IntfCopy or _InstCopy helper functions. There is very little difference between functions that handle interface references and ones that handle object references, so we can freely focus on the former to illustrate assignment behavior.


       
function _IntfClear(var Dest: IInterface): Pointer;
var   P: Pointer; begin   Result := @Dest;   if Dest <> nil then   begin     P := Pointer(Dest);     Pointer(Dest) := nil;     IInterface(P)._Release;   end; end; procedure _IntfCopy(var Dest: IInterface; const Source: IInterface); var   P: Pointer; begin   P := Pointer(Dest);   if Source <> nil then     Source._AddRef;   Pointer(Dest) := Pointer(Source);   if P <> nil then     IInterface(P)._Release; end;

If you have multiple threads accessing - reading and writing - the same reference, one thread can easily interfere with the other. Let's say we have a shared reference Data, pointing to a previously created object, one thread that sets that reference to nil, and another thread that tries to take another strong reference from that one with an assignment to another variable Tmp. The first thread will execute the _IntfClear function that will result in object destruction. The second thread, trying to grab a strong reference preventing object destruction, will execute the _IntfCopy function.


       
var

  Data, Tmp: IInterface;



  Data := nil; // Thread 1 -> _IntfClear



  Tmp := Data; // Thread 2 -> _IntfCopy 

If the Source._AddRef line from the _IntfCopy function executes before the call to IInterface(P)._Release manages to decrease the reference count to zero and subsequently calls the object's destructor, all is good. The second thread will successfully capture another strong reference to the object instance. However, the first thread can interrupt the second thread at the wrong moment, just after the Source <> nil check was successfully passed, but before Source._AddRef had the chance to increment the object's reference count. In that case, the first thread will happily destroy the object instance while the second thread will happily grab a strong reference to an already nuked object and you can forget about "happily ever after".

Rule of thumb. Is it thread safe? Assume not.


Example of thread unsafe code


If you want to observe broken ARC in action, you can run the following code and watch the invalid pointer operations dropping in. That code is equally broken on all Delphi compilers, classic or ARC. Please note, when I say broken, I am not implying a bug in the compiler. It is merely an example of thread unsafe code.


       
uses

  System.SysUtils,

  System.Classes;



var

  Data: IInterface;



procedure Test;

var

  Tmp: IInterface;

  i, j: Integer;

begin

  for i := 0 to 1000 do

    begin

      Data := TInterfacedObject.Create;



      TThread.CreateAnonymousThread(

          procedure

          var

            i: Integer;

          begin

            for i := 0 to 10 do Sleep(15);

            Data := nil;

          end).Start;



      for j := 0 to 1000000 do

        begin

          Tmp := Data;

          if not Assigned(Tmp) then break;

          Tmp := nil;

        end;

    end;

end;



begin

  try

    Test;

  except

    on E: Exception do Writeln(E.ClassName, ': ', E.Message);

  end;

  Writeln('Finished');

end.      
 


Have some fun


And at the end, if you want to have some fun, you can visit The Deadlock Empire  and test your multithreading skills at the same time ðŸ™‚

Comments

Popular posts from this blog

Delphi 12.1 & New Quality Portal Released

Assigning result to a function from asynchronous code

Coming in Delphi 12: Disabled Floating-Point Exceptions