Catalog · How the catalog works
DelphiConcurrent
Delphi Concurrent Anti-DeadLock MultiRead FrameWork
- ★ 8 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
DelphiConcurrent
// Project URL : https://github.com/moualek-adlene/DelphiConcurrent
DelphiConcurrent is a new way to build Delphi applications which involve parallel executed code based on threads like application servers. DelphiConcurrent provides to the programmers the internal mechanisms to write safer multi-thread code while taking a special care of performance and genericity.The main goals of this API are the following :
1- Detect and Prevent the DEADLOCKs before they occur :
In concurrent applications a DEADLOCK may occurs when two threads or more try to lock two consecutive shared resources or more but in a different order.
With DelphiConcurrent, a DEADLOCK is detected and automatically skipped - before he occurs - and the programmer has an explicit exception describing the multi-thread problem instead of a blocking DEADLOCK which freeze the application with no output log (and perhaps also the linked clients sessions if we talk about an application server).Example :
Suppose that we have two threads A and B, and two shared resources 1 and 2.</br>
Threads A and B execution sequences are the following :</br>
<table>
<tr>
<td>Thread(A)</td> <td>Thread(B)</td>
</tr>
<tr>
<td>Resource(1).Lock</td> <td>Resource(2).Lock</td>
</tr>
<tr>
<td>Resource(2).Lock</td> <td>Resource(1).Lock</td>
</tr>
<tr>
<td>Some work on resources...</td> <td>Some work on resources...</td>
</tr>
<tr>
<td>Resource(2).UnLock</td> <td>Resource(1).UnLock</td>
</tr>
<tr>
<td>Resource(1).UnLock</td> <td>Resource(2).UnLock</td>
</tr>
</table>When the threads A and B start their parallel execution, the thread A owns exclusive access to the resource 1 while the thread B does the same for the resource 2. The DEADLOCK will occur in the second step, when the thread A will wait indefinitely for getting access to the resource 2 while the thread B wait indefinitely also for getting access to the resource 1. From this moment, the two threads are mutually blocked forever.Every programmer knows that the DEADLOCKs are very difficult to detect because of two factors :</br>A- The probability that a DEADLOCK occurs is not 100% :</br>
A same written multi-threaded code can run in many ways showing a different behaviour sometime or everytime (we can have 100 running threads for example or 100 shared resources in our application).</br>
In the previous example, the thread A may successfully lock the resources 1 and 2 before that the thread B is created or before that the thread B try to lock the resource 2.</br>
This is a big engineering problem, because that means that code quality cannot be guaranteed simply by running test cases. The programmer may deliver to the client a code where he haven't found any error while testing it and some weeks later, the client is furious because the application (may be a server) freezes repeatedly and he must restart it eachtime the problem occurs, and the log file mysteriously doesn't show anything...B- A DEADLOCK leaves no trace :</br>
Threads are like the human beings, they can't talk after they are dead !. Mutually blocked threads will not fire any exception saying 'We are blocked', they will just freeze forever. No exception means also no entry in the log output, so analysing the application log file will not help to discover or resolve the problem.</br>
Moreover, some of the locked resources may be critical which means that these resources may by used globally by the other application threads and this will lead to an overall application failure progressively (deny of service).DelphiConcurrent can protect the threads from DEADLOCKs by a correct monitoring of the lock/unlock sequences at runtime and an explicit exception raised when the problem is detected. This is done with a probability of 100% even if the DEADLOCK doesn't occurs, hence the code quality (the thread safety part) is guaranteed at the project end.2- Detect the Remaining Locks :
Every time a programmer locks a shared resource on a concurrent application he must explicitly don't forget to unlock this resource to make it available again to the other threads in the application. But a programmer is a human being and in a big or complex application he can easily forget to do that in some cases which will make one or more resources definitely inavailable to the other threads with no exception (no log entry). It happens sometimes also that the programmer inserts -by mistake- a lock command in his source code instead of the unlock command needed. DelphiConcurrent handles this problem also by keeping trace of the programmer lock/unlook sequences and throwing an exception every time a shared resource is not unlocked at the good moment.3- Detect the Bad Unlocks Sequences :
This is more an additional comfort than a real problem, because a programmer is not theoretically constrained to unlock a set of shared resources in a special order, but it is a good practice to do that in the correct order (which is the reverse of the lock order). So, DelphiConcurrent will also throw an exception every time the good unlock order is not respected.4- Provide a High-Performance Threading Model based on the MREW model :
DelphiConcurrent implements the Multi-Read Exclusive-Write (MREW) threads synchronization model. This model of parallel threads execution is more efficient than the other synchronization schemas based on Critical-Sections or Monitors because it doesn't prevent parallel threads from reading at the same time from a shared resource, exclusive access is only needed when writing to the resource. The MREW is therefore the default synchronization model used in DelphiConcurrent, however the other synchronization schemas (Critical-Sections and Monitors) are also implemented and can be easily used if necessary.DelphiConcurrent API Presentation :
This API is encapsulated in the Delphi Unit named "DelphiConcurrent.pas".</br>
A running Delphi example (Project and Binary) is available in the GitHub repository in order to see how this API works. The example is build on Delphi 10.1 Berlin Edition.DelphiConcurrent uses the concept of "Lock on resource" rather than "Lock on code" because it is impossible to guarantee a zero-fault multi-threaded code if we leave the control to the programmer (or the development team) for managing the kind of problems described before.
The DEADLOCKs or Forgotten-LOCKs especially must be detected By Design and not just if they occurs someday.Each shared resource is represented in DelphiConcurrent by an instance of a class derived from the class TDCProtected. We can use for example the TDCProtectedList sub-class to have a thread-safe TList that way.</br>
This shared resource will not be accessible directly at runtime, rather it will be encapsulated in an object of the class TDCProtector which associates the protected resource to some lock object or mechanism. This last may be a Monitor or an instance of the class TCriticalSection or TDCMultiReadExclusiveWriteSynchronizer.In theory, if we want to avoid the DEADLOCKs in our concurrent code, we must ensure that the resources are locked in the same order everytime we access them, in every portion of our source code. But in practice, this is very difficult to achieve, because a real application is made of different modules (units) each one containing perhaps some shared resources, and some of these resources may be allocated dynamically (not enumerated at design step), and as that was not enough there is maybe many programmers involved in the project and no programmer has a clear vision from project start of what lock order must be respected by all the team.</br>
This is way each resource in DelphiConcurrent will have a global order which determines at which moment we can access it compared to other resources (of all application modules). This global order is stored in the read-only property TDCProtector.LockOrder and will correspond (by convention) to the global creation order of shared resources at runtime.DelphiConcurrent introduces a Local-Execution-Context class named TDCLocalExecContext to monitor the overall lock/unlock sequences in each running thread. As indicated by his name, each allocated context is local to his thread (Don't try to share those contexts between threads). The TDCLocalExecContext keeps an eye on each lock/unlock instruction and will throw an exception if any problem is detected. It is it's responsibility to detect and prevent the DEADLOCKs.A typical DelphiConcurrent API use will be the following :</br>// Some shared resource declared and allocated somewhere (in main-unit for example)
var
GSharedResource: TDCProtector;
begin
// we need for example, a thread-safe TList, and
// we choose to protect it with a Multi-Read Exclusive-Write Synchronizer
GSharedResource := TDCProtector.Create(TDCProtectedList, ltMREW);
try
Thread(X, GSharedResource).Start;
Thread(X).WaitFor;
finally
GSharedResource.Free;
end;
end;// Some Thread(X) implementation
var
LExecContext: TDCLocalExecContext;
LResourcePointer: TDCProtectedList;
begin
LExecContext := TDCLocalExecContext.Create;
try
try
LResourcePointer := GSharedResource.Lock(LExecContext);
try
<Some work with the shared resource LResourcePointer ...>
finally
GSharedResource.UnLock(LExecContext);
end;
except
on e1:Exception do NotifyToUI('Exception "' + e1.ClassName + '" : ' + e1.message);
end;
finally
LExecContext.Free;
end;
end;The running Delphi example available in the GitHub repository handles some advanced topics like how to automatically react to a DEADLOCK. We can for example, not just fill a log file or show a message on the screen but also release any previously locked resources (unlock them) to -at least- restore those resources to the other threads (look at the implementation of the procedure TDeadLockTester.Execute in the ThreadsUnit.pas for that).DelphiConcurrent Exception Classes :
All the DelphiConcurrent exception classes derive from the TDCException class.</br>
We distinguish the following exceptions :</br>1- TDCDeadLockException :</br>
The DelphiConcurrent API will throw this kind of exception whenever the global lock order is not respected. Programmers involved in a project managed with the DelphiConcurrent API must lock the shared resources following their creation order in memory. Which means that if a Resource 1 is created before a Resource 2 in memory than every thread which need to work on both resources at the same time must lock the Resource 1 before the Resource 2 and not the reverse.2- TDCRemainingLocksException :</br>
Before a TDCLocalExecContext is destroyed, it will check that there is no remaining active locks and will throw this kind of exception if the problem is detected.3- TDCBadUnlockSequenceException :</br>
The DelphiConcurrent API will throw an exception every time the good unlock order is not respected (which is the reverse of the lock order).Work is in progress to achieve this API. Currently, many basic Delphi classes have been wrapped with this API (TList, TObjectList, TStack, TObjectStack, TQueue and TObjectQueue). Next step will be to do the same for generic versions of those classes (System.Generics.Collections).
