What is thread safety anyway?
Dalija Prasnikar - 11/Dec/2017
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.
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.
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:
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.
[/SHOWTOGROUPS]
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]