I wrote an FAQ on a third-party website which pertains to thread-protecting objects in Delphi. What I'd like to know is if this thread-protection approach is accurate, or if I should change anything about it. I don't intend to ask about the FAQ in general, just the aim at the actual topic of multi-threading.
This code comes from common practices of mine, and I'm starting to wonder if these practices are good or bad. I've used this TLocker
object for a long time, and would like to know if there's anything wrong with any of this approach.
The FAQ content:
There are many ways to thread-protect objects, but this is the most commonly used method. The reason you need to thread-protect objects in general is because if you have more than one thread which needs to interact with the same object, you need to prevent a deadlock from occuring. Deadlocks cause applications to freeze and lock up. In general, a deadlock is when two different threads try to access the same block of memory at the same time, each one keeps repeatedly blocking the other one, and it becomes a back and forth fight over access.
Protection of any object(s) begins with the creation of a Critical Section. In Delphi, this is TRTLCriticalSection in the Windows unit. A Critical Section can be used as a lock around virtually anything, which when locked, no other threads are able to access it until it's unlocked. This ensures that only one thread is able to access that block of memory at a time, all other threads are blocked out until it's unlocked.
It's basically like a phone booth with a line of people waiting outside. The phone its self is the object you need to protect, the phone booth is the thread protection, and the door is the critical section. While one person is using the phone with the door closed, the line of people has to wait. Once that person is done, they open the door and leave and the next comes in and closes the door. If someone tried to get in the phone booth while someone's using the phone, well, it's obvious that would lead to a fight. That fight would be a deadlock.
It is still a risk to cause a deadlock even when using this method, for example, someone trying to use the phone when someone else is using it. If for any reason a thread attempts to access the object without locking it, and while another thread is accessing it, you will still run into issues. So, you need to use this protection EVERYWHERE that could possibly need to access it. Remember, when you lock this, nothing else is able to lock it again (under the promise that all other attempts would also be using this same Critical Section). The locking thread must unlock it before it can be locked again.
Okay, so into the code. The four calls you will need to know are:
- InitializeCriticalSection() - Creates an instance of a Critical Section
- EnterCriticalSection() - Engages the lock
- LeaveCriticalSection() - Releases the lock
- DeleteCriticalSection() - Destroys an instance of a Critical Section
First, you need to declare a Critical Section somewhere. It's usually something that would be instantiated for the entire duration of the application, so I'll assume in the main form.
TForm1 = class(TForm)
private
FLock: TRTLCriticalSection;
Then, you need to initialize (create) your critical section (presumably in your form's constructor or create event)...
InitializeCriticalSection(FLock);
Now you're ready to use it as a lock. When you need to access the object it's protecting, you enter the critical section (locking it)...
EnterCriticalSection(FLock);
Once you have done this, it is locked. No other thread other than the calling thread is able to access this lock. Remember, technically other threads are able to access the object (but would potentially cause a deadlock), so make sure all other threads are also first trying to lock it. That's the goal of this lock.
Once you have done everything you need in this thread, you leave the critical section (unlocking it)...
LeaveCriticalSection(FLock);
Now, it is unlocked and the next thread which may have attempted a lock is now able to completely lock it. That's right, while a critical section is locked, it remembers any other lock attempt, and once the calling thread unlocks it, the next one in the queue automatically acquires this lock. Same concept as a line of people waiting outside a phone booth.
When you're all finished with the object, make sure you dispose of this critical section (presumably in your form's destructor or destroy event)...
DeleteCriticalSection(FLock);
And those are the fundamentals. But we're not done yet.
It's highly advised that you should always use a try..finally block when entering/leaving a critical section. This is to ensure that it gets unlocked, in case of any unhandled exceptions. So, your code should look something like this:
EnterCriticalSection(FLock);
try
DoSomethingWithTheProtectedObject;
finally
LeaveCriticalSection(FLock);
end;
Now, the last thing you may wish to do is wrap this up in a nice simple class to do the work for you. This way, you don't have to worry about all the long procedure names whenever you use this lock. This object is called TLocker
, which protects an object instance for you. You would need to create an instance of this for every object instance you need to protect. So, instead of declaring something like FMyObject: TMyObject;
you would do it like FMyObject: TLocker;
and then give the object instance to it upon its creation.
type
TObjectClass = class of TObject;
TLocker = class(TObject)
private
FLock: TRTLCriticalSection;
FObject: TObject;
public
constructor Create(AObjectClass: TObjectClass); overload;
constructor Create(AObject: TObject); overload;
destructor Destroy;
function Lock: TObject;
procedure Unlock;
end;
constructor TLocker.Create(AObjectClass: TObjectClass);
begin
InitializeCriticalSection(FLock);
FObject:= AObjectClass.Create;
end;
constructor TLocker.Create(AObject: TObject);
begin
InitializeCriticalSection(FLock);
FObject:= AObject;
end;
destructor TLocker.Destroy;
begin
FObject.Free;
DeleteCriticalSection(FLock);
end;
function TLocker.Lock: TObject;
begin
EnterCriticalSection(FLock);
Result:= FObject; //Note how this is called AFTER the lock is engaged
end;
procedure TLocker.Unlock;
begin
LeaveCriticalSection(FLock);
end;
The first constructor TLocker.Create(AObjectClass: TObjectClass);
expects a class type to be specified (such as TMyObject
).
The other constructor TLocker.Create(AObject: TObject);
expects an instance of an object already created. Bear in mind that if you do use this constructor, you should no longer refer directly to that object after passing it into this constructor.
In both of these cases, your object which is being protected will be automatically free'd when the TLocker is free'd.
Let's assume the second constructor of the two. Once you've created an instance of this class with your initial actual object reference, never reference to the actual object again. Instead, whenever you need to use that object, pull it out of the TLocker instance by locking it. Using a try..finally block, make sure you also unlock it once you're done, or else the lock is stuck.
So, using the class above, you can access an object like this:
procedure TForm1.FormCreate(Sender: TObject);
var
O: TMyObject;
begin
O:= TMyObject.Create;
FMyObjectLocker:= TLocker.Create(O);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FMyObjectLocker.Free; //Your original object will be free'd automatically
end;
procedure TForm1.DoSomething;
var
O: TMyObject;
begin
O:= TMyObject(FMyObjectLocker.Lock);
try
O.DoSomething;
//etc...
finally
FMyObjectLocker.Unlock;
end;
end;
As you see, I acquire the object at the same time as locking it. This should be the only possible way to access this object. Just make sure you always unlock it when you're done. Notice also how I also don't define the actual protected object in the class - to prevent accidental direct calls to the object.
TCriticalSection
declared inSyncObjs
unit that gives you a more object-oriented approach of working with critical sections. Using this or the routines inWindows
units is a matter of style and preference, but I decided to inform you about it. \$\endgroup\$TLock
class (for instance,TLock<T>
). In more modern Delphi flavors you would be able to have a method that receives a anonymous method that would wrap thetry..finally
block, what would make it impossible to one to forget to unlock the protected resource. Those are just ideas for your consideration. \$\endgroup\$