Articles What is "Thread" safety anyway? by Dalija Prasnikar

emailx45

Местный
Регистрация
5 Май 2008
Сообщения
3,571
Реакции
2,439
Credits
574
What is thread safety anyway?
Dalija Prasnikar - 11/Dec/2017
[SHOWTOGROUPS=4,20]
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!

Microsoft Docs:
Для просмотра ссылки Войди или Зарегистрируйся

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;
begin
  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;
...
implementation
...
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;

[/SHOWTOGROUPS]