#1 2011-08-29 07:22:14

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,206
Website

Multi-threading and Delphi

Writing working multi-threaded code is not easy - it's even hard, as as a Delphi expert just wrote in his blog.

In fact, the first step into multi-thread application development could be:

"protect your shared variables with locks (aka critical sections), because you are not sure that the data you read/write is the same for all threads".

The CPU per-core cache is just one of the possible issues, which will lead into reading wrong values. Another issue which may lead into race condition is two threads writing to a resource at the same time: it's impossible to know which value will be stored afterward.

Since code expects the data to be coherent, some multi-thread programs may behave wrongly. With multi-threading, you are not sure that the code you write, via individual instructions, is executed as expected, when it deals with shared variables.

InterlockedExchange / InterlockedIncrement functions are low-level asm opcodes with a LOCK prefix (or locked by design, like the XCHG EDX,[EAX] opcode), which will indeed force the cache coherency for all CPU cores, and therefore make the asm opcode execution thread-safe.

For instance, here is how a string reference count is implemented when you assign a string value (see _LStrAsg in System.pas - this is from our optimized version of the RTL for Delphi 7/2002 - since Delphi original code is copyrighted):

    MOV     ECX,[EDX-skew].StrRec.refCnt
    INC     ECX   { thread-unsafe increment ECX = reference count }
    JG      @@1   { ECX=-1 -> literal string -> jump not taken }
.....
@@1: LOCK INC [EDX-skew].StrRec.refCnt { ATOMIC increment of reference count }
    MOV     ECX,[EAX]
...

There is a difference between the first INC ECX and LOCK INC [EDX-skew].StrRec.refCnt - not only the first increments ECX and not the reference count variable, but the first is not thread-safe, whereas the 2nd is prefixed by a LOCK therefore will be thread-safe.

By the way, this LOCK prefix is one of the problem of multi-thread scaling in the RTL - it's better with newer CPUs, but still not perfect.

So using critical sections is the easiest way of making a code thread-safe:

var GlobalVariable: string;
    GlobalSection: TRTLCriticalSection;
procedure TThreadOne.Execute;
var LocalVariable: string;
begin
...
  EnterCriticalSection(GlobalSection);
  LocalVariable := GlobalVariable+'a'; { modify GlobalVariable }
  GlobalVariable := LocalVariable;
  LeaveCriticalSection(GlobalSection);
....
end;
procedure TThreadTwp.Execute;
var LocalVariable: string;
begin
...
  EnterCriticalSection(GlobalSection);
  LocalVariable := GlobalVariable; { thread-safe read GlobalVariable }
  LeaveCriticalSection(GlobalSection);
....
end;

Using a local variable makes the critical section shorter, therefore your application will better scale and make use of the full power of your CPU cores. Between `EnterCriticalSection` and `LeaveCriticalSection`, only one thread will be running: other threads will wait in `EnterCriticalSection` call... So the shorter the critical section is, the faster your application is. Some wrongly designed multi-threaded applications can actually be slower than mono-threaded apps!

And do not forget that if your code inside the critical section may raise an exception, you should always write an explicit try ... finally LeaveCriticalSection() end; block to protect the lock release, and prevent any dead lock of your application.

Delphi is perfectly thread-safe if you protect your shared data with a lock, i.e. a Critical Section. Be aware that even reference-counted variables (like strings) should be protected, even if there is a LOCK inside their RTL functions: this LOCK is there to assume correct reference counting and avoid memory leaks, but it won't be thread-safe. To make it as fast as possible, see this article.

The purpose of `InterlockExchange` and `InterlockCompareExchange` is to change a shared pointer variable value. You can see it as a a "light" version of the critical section to access a pointer value.

In all case, you should either:
- Write simple threads with no shared data at all (make a private copy of the data before the thread starts, or use read-only shared data - which is thread-safe by essence);
- Use critical sections, as short as possible, to protect the shared data or process;
- Or call some well designed and proven libraries - like http://otl.17slon.com - which will save you a lot of debugging time.

Offline

#2 2011-08-29 10:50:55

TPrami
Member
Registered: 2010-07-06
Posts: 106

Re: Multi-threading and Delphi

Hello,

Thread safety is one thing.

But how you would move data or signal between threads.

I think the

Lock;
  CallUpdateWithData(Data);
UnLock;

is little bit too Heavy in many cases. Maybe sending messages between threads? But how much that will have Overhead (time)??

TSimpleEvent is easy if you can just wait for the processed data to arrive, but it is not that good always. But it will not provide the data.

I've been thinking that maybe messages would be way to go, the the thread would not lock each other all the time...  also I think that thjere should be simple (set) of class to be wrapper around the message sending (etc) API.

-Tee-

Offline

#3 2011-08-29 16:03:09

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,206
Website

Re: Multi-threading and Delphi

You are perfectly right.

But if you start working with thread synchronization, I see especially two kind of implementation:
- Use a dedicated library like http://otl.17slon.com to handle the messaging;
- Use a pure stateless approach: in this case, you won't have to synchronize anything - any thread may be in whatever state.

In our framework, we used a RESTful approach, so mainly rely on the 2nd solution. Not the best for performance, but the best for multiple users over a slow network, or for HTTP applications.

Offline

#4 2011-08-30 04:30:21

TPrami
Member
Registered: 2010-07-06
Posts: 106

Re: Multi-threading and Delphi

Hello,

Can't use OmnithreadLibrary, because I am using D7.

-TP-

Offline

Board footer

Powered by FluxBB