Welcome to another of my articles. This time its multithreading. A topic which seems to fill a lot of programmers with dread, but if used properly it can produce some great results. Traditionally it was the realm of high end servers with multiple CPU's, but with the big two now producing multicore chips that are accessible to gamers, this is a topic which will soon become something every games developer should be considering.
Just in case you don't know what a thread is, the definition I like to use is one of those annoyingly recursive IT definitions... a thread is a single thread of execution within your application. I'm not going to go into the details of multithreading such as time slicing etc., so if you really want to know about the nuts and bolts, check out this Wikipedia page on
multithreading.
What I am going to do however is show you how to implement threads with Delphi and provide some details about the things you need to be considering when writing multithreaded applications.
But to get started, lets consider when and why you might want to make your application multithreaded. As an example, I'm going to use our competition entry from the PGD Annual Competition 2006. The basic concept of our entry was to build a game engine that could be used to make 2D top down tiled games... kind of retro RPG style. To make it flexible it had event handlers for practically everything, these were Delphi Web Script II scripts, and for the enemies, it had an A* path finding system.
There was obviously more to it, but these are the two items of importance for this article.
First, lets look at the scripting of the event handlers. In the early stages, comments were made about the event handling. The renderer would block while a script was being executed. Of course, this was going to happen because everything was running inside the main VCL thread, so when we moved (a process handled by our main TDXTimer) we might run an event handling script. Because our main TDXTimer also handled the rendering, everything stopped until the script had finished. This might not be a problem in certain situations, but in ours it was a huge problem. Some of the things we wanted to do within our scripts was to change the visibility of the player, change the players location and to change the state of map cells. Of course, we did all that, but the player only ever saw the final result. As a consequence, some of our work was effectively lost since the player never saw it.
To fix this, I decided that the scripts should be executed by a seperate thread. When I needed to run a script, I stopped the state of the game changing in response to player input and then dropped a pointer to the compiled script into a queue in a script executor. It then ran the script in a seperate thread leaving the main VCL thread free to handle the rendering etc. So, when the script made a change to the state of the player or the game, the player could see the results as the renderer was free to run. When all the scripts in the queue had been executed, the player was once again able to influence the state of the game.
That sounds like a complicated way of achieving this, but it worked really well, and the performance of it was very acceptable on my humble Athlon 800. The A* path finder was a more sophisticated system. It provided three different queues. Low, medium and high priority. When an enemy needed a path, it would submit a path request to the pathfinding thread, along with a callback handler. The pathfinder was able to suspend itself if there were no path requests in its queues (thus saving scheduling time), if it was suspended when the path request was made, it was kick started by the request. It then checked its queues and began processing the jobs, finding paths (or not) for each request on a first come first served priority basis. When it completed a path, it would run a callback that came in with the job request. The path was passed back to the object that requested the path as a parameter of this callback.
Again, it sounds complicated, but it worked really well and, like the script executor, its performance was more than acceptable.
So why is multithreading a good idea?
I suspect a lot of people would say, its not a good idea either because of the apparent complexity multithreading implies or because to get the most out of it, you need to run multiple cores. But as I've already mentioned, multicore machines are cheaper than they ever have been. The masses now have access to technology that was once the domain of the high end server. So suddenly, we find ourselves in an age where we can gain great performance benefits from this.
If you write your game as a standard single threaded application, then running it on a multicore machine will make no difference to the overall performance of the game (you should notice some performance increase as the OS shares other tasks across the multiple cores, but the game itself will not benefit from this). If on the other hand, you take the time and do a good job of making your game multi-threaded, you shouldn't notice too much of a performance hit when running it on a single core machine, but run it on a multicore system and you should notice a good performance increase as the OS makes the most of the extra cores, and as a consequence your threads get more clock cycles.
So whats a 'good job' in multithreading terms?
Well that depends on context. The browser based game I run requires a whole bunch of calculations to be performed every hour in order to make time pass in the game universe. In this context, a good job is making sure that the two CPU's in the server that runs it are running at 100% the whole time this process is running (this server actually spends most of its life doing nothing, idling at 0% utilization). Only then can I ensure we have the fastest completion times as I know that the two processing threads I spawn are running concurrently, one each on the two cores.
In the context of a client side game, it requires something different. In server side applications (like the one I described), it probably doesn't matter too much if other processes suffer when your application is running since the client probably won't see this. But, when multithreading a client application, you must take steps to minimise the chances of single core machines noticing slow down whilst threads other than the main VCL thread are executing, but thats relatively easy to achieve by ensuring that processor intensive threads (such as pathfinding) relinquish control of the CPU periodically or run at a lower priority than your main thread (or both). Since these measures are controlled entirely by software, its easy to adjust them when running on a multicore machine.
Finally, before we get into some code, one of the things that causes alot of problems for people who are new to multi-threading is resource protection. Consider the implications of this code...
x:=100/factor;
Possible divide by 0? Now throw into the equation...
fFactor^:=0;
for loop:=low(fData) to high(fData) do
fFactor^:=fFactor^+fData[loop];
*deep breath* If execution (by a seperate thread) of the second code fragment stops after fFactor^:=0; (as could potentially happen on a single core machine), or both code fragments are running simultaneously (on a multicore machine) and it just happens that x:=100/factor is executed after the fFactor^:=0 but before the loop starts and fFactor is indeed a pointer to the factor variable from the first segment... *boom*
Ok, not quite boom, but you get the picture. This is where resource protection comes in. More of that later... for now, lets look at a basic thread.
Creating and Terminating Threads
Now we've covered the background, lets look at some code. First off, the basics of a thread.
interface
uses
classes;
type
TVerySimpleThread = class(TThread)
protected
fCounter : integer;
procedure execute; override;
end;
implementation
procedure TVerySimpleThread.execute;
begin
while not self.terminated do
fCounter:=fCounter+1;
end;
The most important part of any thread class is the 'execute' method as this contains the core code your thread will execute when its running. In this simple example all we want our thread to do is constantly increment the counter (fCounter) while its running. Thats where the 'while' comes in. Without it, our thread would run, increment the counter once and then terminate when we exit the 'execute' method (this action may be what you want, and does allow you to create 'fire and forget' threads for handling one off operations in the background), but with the 'while', once the thread starts it will sit there incrementing our counter until it is explicitly terminated by calling the 'terminate' method.
But what happens next?
Well that depends on how you want to handle things. You have several different options relating to terminating and cleaning up your thread and your choice will be governed by how you are using the thread object. The options you have are 'fire and forget', 'application notification', 'terminate on demand'.
Fire and Forget
To implement a fire and forget thread, then you need to consider the 'freeOnTerminate' property, but you also need to ensure that your application either does not need to access these spawned threads or if it does, that it can cope with potentially not being able to gain access to the thread object (it may have terminated and been freed). Once your thread has done its job, it can end by leaving the 'execute' method.
// The nice way to create a 'fire and forget' thread (use a temporary variable)
myThread:=TMyThreadClass.create(false);
myThread.freeOnTerminate:=true;
// And the dirty (true fire and forget) way...
TMyThread.create(false).freeOnTerminate:=true;
Of course you could also handle this by setting 'freeOnTerminate' in the constructor of your thread class. By default, 'freeOnTerminate' is false, so if you want to have the thread free itself, then make sure you set this flag.
Application Notification
If you want the thread to let your main application to know its terminated, you can provide a handler for the '
OnTerminated' event. This is a standard TNotify event (procedure(sender:TObject) of object). When the thread terminates, this handler is called and you can take any action you deem appropriate (including freeing up the thread object). But, bear in mind that if you've already set the 'freeOnTerminate' property to true and you free the thread in the
OnTerminate event handler, you will be faced with an access violation as the system attempts to free the thread itself when it returns from the event handler.
...
myThread.onTerminate:=form1.myOnTerminateHandler;
...
procedure TForm1.myOnTerminateHandler(sender:TObject);
begin
// Sender points to the TThread descendant that is ending
sender.free;
end;
As a side note, its no good changing your mind about whether to have the thread free itself when you are in the 'onTerminate' event handler. By the time your event handling code is executed, the thread has already looked at the 'freeOnTerminate' property and stored its value in a variable (this is just in case you free it in this event handler... the code doesn't cause an AV when its deciding whether to free the thread or not). So, if you want to have a thread free itself, you must decide this and setup 'freeOnTerminate' BEFORE the thread terminates.
Terminate on Demand
The final key choice you have for handling the termination of a thread is what I would call 'Terminate on Demand'. This method of terminating a thread does not rely on using the 'freeOnTerminate' property or the 'onTerminate' event handler as you explicitly terminate the thread and then wait for it to finish. You can use 'freeOnTerminate' or the 'onTerminate' event handler to free up the thread, but if you are explicitly requesting the termination of the thread, its just as easy to free it up yourself when its completely finished.
...
// Create our thread and grab it
myThread:=TMyThread.create(false);
...
...
// Terminate our thread
myThread.terminate;
// Wait for it to finish up
myThread.waitFor;
// And free it
myThread.free;
...
Where possible, I would advise that you keep careful track of your threads in order to ensure that they are all terminated (you're application could hang on shutdown if a thread is still running or is sitting there suspended) and that any resources are cleaned up. This is especially important if you are writing a multithreaded service that spawns lots of threads.
Supending and Resuming Threads
You will by now have noticed the boolean parameter being passed to the threads 'create' method. This is the 'createSuspended' flag. It tells the operating system the initial state of the thread once it has been created. It can either be running (createSuspended=false) or suspended (createSuspended=true).
Why is this relevant?
Well, consider this simple thread class.
interface
uses
classes;
type
TAVThread = class(TThread)
protected
fPInt : ^integer;
procedure execute; override;
public
constructor create;
end;
implementation
constructor TAVThread.create;
begin
inherited create(false);
sleep(100);
new(fPInt);
end;
destructor TAVThread.destroy;
begin
dispose(fPInt);
inherited;
end;
procedure TAVThread.execute;
begin
while not self.terminated do
fPInt^:=fPInt^+1;
end;
If you create an instance of this thread, you will notice that you get an access violation in the 'execute' method. This is because the thread wasn't created and placed in its suspended state. When the thread that called the constructor encounters the sleep(100), it yields to the OS which allocates time to the newly created thread. As a consequence, the 'execute' method is called. Obviously, when that happens, it will try to operate on 'fPInt' which is still waiting to be initialised by the constructor. Net result... *boom*
This is where 'createSuspended' comes in.
interface
uses
classes;
type
TNoAVThread = class(TThread)
protected
fPInt : ^integer;
procedure execute; override;
public
constructor create;
end;
implementation
constructor TNoAVThread.create;
begin
inherited create(true);
sleep(100);
new(fPInt);
self.resume;
end;
destructor TNoAVThread.destroy;
begin
dispose(fPInt);
inherited;
end;
procedure TNoAVThread.execute;
begin
while not self.terminated do
fPInt^:=fPInt^+1;
end;
In this case, we have created our thread in its 'suspended' state. Now when the thread that called the constructor encounters the sleep request, it will yield to the OS (as before), but this time around, it will ignore the new thread because as far as it is concerned, its suspended. We can now fully initialise our thread object and then start executing by calling the 'resume' method.
Of course, you don't have to call 'resume' within the constructor. You may want to create a thread and then have it sit there idle until its needed. In that case, simply ommit the 'self.resume' from the constructor and call the 'resume' method when you want the thread to begin executing.
...
myThread.create(true);
...
...
// We need our thread to get busy
myThread.resume;
...
...
// And now we want it to sit idle
myThread.suspend;
...
Just a note about why in some cases I've overriden the constructors parameters... its my personal preference to override the constructor and remove the 'createSuspended' parameter because I generally try and design my threads to be as simple to use as possible, so any configuration I can put in the constructor, I do. I've also been bitten by the issue I described above, with the 'execute' method firing up before the constructor has completely finished, so I got myself into the habbit of creating all my threads suspended and then resuming them manually as and when I need to.
Although this example is somewhat artificial because its unlikely you'd put 'sleep' in the constructor, if the constructor takes too long, the effect is the same. The 'execute' method could find itself trying to use variables etc. that are not yet fully initialised. On a single core machine you can get quite a while before the newly created thread gets some CPU time (I have just tried it on my Athlon 800 and I got around 1 second before I experienced an AV - For the curious, I had the constructor perform 2mil repititions of the calculation i=((i*2)/3) ), but on a dual (or more) core machine, this time will be radically reduced as the OS can shuffle threads around the multiple cores of the machine.
The other option you have to get around this problem of threads running before they are initialised is to handle the initialisation and cleanup in the 'execute' method itself and create the thread in its running state (createSuspended=false).
procedure TMyThread.execute;
begin
// Initialise here
try
...
// Main execute code here
...
finally
// Cleanup here
end;
end;
How you decide to handle this aspect of threads will ultimately depend on how you are using them and to a certain extent your own preferences which will develop as you get more experienced with multithreading.
You may be asking yourself why you would want to suspend a thread when the whole idea is to have the thread running in the background. The answer is simple, performance. This is of particular concern when the application is running on a single core machine.
Whilst a thread is active, the OS will allocate CPU time to it... even if the thread simply checks whether it has a job to do and then yields to the OS, that takes time... time which could probably be better spent doing other things (rendering for example). But, when you suspend the thread, the OS simply skips it. This saves the time it takes to perform two context switches and the time the thread uses in determining it has nothing to do before yielding to the OS as it waits for a job.
This polling method of checking whether we need to do anything and yielding to the OS if there isn't can (if you have a lot of threads) result in slowdown that could affect the performance of your game as the system is constantly switching threads to check for work. It can also lead to delays in the commencement of job processing. This is because of how sleep works. If you say sleep(10), your thread will suspend its execution for 10ms. If you say sleep(1000), your thread suspends its execution for 1sec. Sleep(10) will result in a lot of context changes that could slow down your game. Sleep(1000) on the other hand won't have so many context switches, but could mean that in the worst case, the job you want your thread to do is effectively put on hold for 1sec.
This is why you would suspend threads that don't have anything to do. If you have multiple cores, you can get away with leaving more threads in their running state, but this could result in poor performance on single cored machines as the OS allocates time to the threads you haven't suspended. So bear this in mind when designing a multithreaded game.
So lets look at how you might use 'suspend' and 'resume' to ensure your thread is only running when it has work to do. In this example, I've created a simple job processing thread.
procedure TMyThread.execute;
var
aJob : TMyThreadsJobDescription;
begin
repeat
if (self.workToDo) then
begin
aJob:=self.getNextJob;
// Process the job here
try
aJob.onJobCompleted(self,aJob);
except
try
aJob.free;
except
end;
end;
end
else
begin
if not self.terminated then
self.suspend;
end;
until (self.terminated);
end;
procedure TMyThread.stop;
begin
self.terminate;
if (self.suspended) then
self.resume;
end;
procedure TMyThread.addJob(aJob:TMyThreadsJobDescription);
begin
// Add 'aJob' to the job queue
if (self.suspended) then
self.resume;
end;
Firstly, the exact content of aJob is not really important. It is an object that contains the data required by your thread to perform the required actions. Part of this is an event handler that your thread will call when it has finished processing the job. This isn't the only way to get data out of a thread, but in the context of a job processing thread, its a nice clean mechanism for letting the job creator know that its job is finished. You will of course notice the multiple try..except blocks around the event raising code. This is done just in case the object that created the job has been destroyed (an enemy requested a path but was blown up in the interim for example). It stops any exceptions raised in that block terminating the thread prematurely and ensures that we do our best to free up the job object that would ordinarily be cleaned up by the job creator when its finished with it.
The other two methods handle killing the thread and adding jobs.
The 'terminate' method we would normally use for terminating the thread only sets the 'terminated' flag to true. It does not resume a suspended thread, so if you don't resume the thread yourself, you could be waiting an awfully long time for your thread to terminate. For this reason, I've created a new method 'stop'. This calls 'terminate' and then checks whether or not the thread is suspended... if it is, it calls 'resume'. 'addJob' performs a similar check when a new job is added to the job queue.
So, with this simple approach, we have a thread that will fire up, and then suspend itself when it has nothing to do (saving clock cycles), but when we add a job or kill it, we have very little delay since it gets straight to work when we resume it.
Synchonisation
This is an important issue and one that must be considered when multithreading. It starts off with this statement... not everything in your application is thread aware (or thread safe). Even today, the majority of the VCL isn't. If you aren't aware of this, it can cause headaches that can be a real problem to track down.
I found this out quite a while ago. I was tinkering around with a multithread graphics app. It was nothing fancy, but it used multiple threads to plot different sections of an image. It ran perfectly on my machine and seemed to complete its task slightly quicker than its single threaded counterpart. Great I thought. I showed it to a colleague and it produced some very interesting results. I ran it again on my machine... no problems. His machine. *boom*.
The only difference was that his machine was a dual CPU system so instead of running piecemeal one after the other (as they did on my machine), both threads were running simultaneously and thats when the problems started. Both threads were accessing a TCanvas simultaneously and it really didn't like it very much.
Fortunately though, mechanisms are provided to handle this. So lets take a look at the major player... 'synchronize'
'Synchronize' is used to run a method of a thread within the context of the main VCL thread of your application. So if you are going to manipulate the majority of the VCL object hierarchy, or you're not absolutely 100% certain that the objects you'll be working with are thread safe, you would be advised to use 'synchronize'. Although the Delphi help does cover this topic, its not totally explicit as to which components are and which aren't thread safe.
I normally play it safe which simply means that if I'm going to be working with VCL objects or other components provided by 3rd parties, I synchronize. By all means try without synchronize, but as I found, this problem only manifested itself when the application was running on a multicore machine (if you can try it out on your machine... you lucky devil ).
So how do we use 'synchronize'?
Its actually pretty straight forward, but it does have certain limitations. The main one being that you can only call procedures that don't take any parameters.
As an example, lets consider our job processing thread example. When the job completes, the callback is executed in the context of this thread. The thread itself doesn't have a clue what that call back is going to do.. it could be accessing sections of code that aren't thread aware and this could spell disaster. So lets rework the job processing thread to ensure that it plays nice.
interface
uses
classes, syncObj;
type
TMyJobResult = class(TObject);
TMyJobCompletedCallback = procedure(sender:TObject;jobResult:TMyJobResult) of object;
TMyJobDescription = class(TObject)
protected
fCallback : TMyJobCompletedCallback;
public
property callback:TMyJobCompletedCallback read fCallback write fCallback;
end;
TMyJobProcessor = class(TThread)
protected
fJobResult : TMyJobResult;
fJob : TMyJobDescription;
fJobListCS : TCriticalSection;
fJobList : TList;
procedure execute; override;
public
constructor create;
destructor destroy; override;
procedure doCallback;
procedure stop;
procedure addJob(aJob:TMyJobDescription);
end;
interface
constructor TMyJobProcessor.create;
begin
inherited create(true);
self.freeOnTerminate:=true;
fJobListCS:=TCriticalSection.create;
fJobList:=TList.create;
end;
destructor TMyJobProcessor.destroy;
begin
while (fJobList.count>0) do
begin
try
TMyJobDescription(fJobList[0]).free;
except;
end;
fJobList.delete(0);
end;
try
fJobList.free;
except
end;
try
fJobListCS.free;
except
end;
inherited;
end;
procedure TMyJobProcessor.doCallback;
begin
if (assigned(fJob.callback)) then
begin
try
fJob.callback(self,fJobResult);
except
fJobResults.free;
end;
end;
end;
procedure TMyJobProcessor.addJob(aJob:TMyJobDescription)
begin
fJobListCS.acquire;
try
fJobList.add(aJob);
finally
fJobListCS.release;
end;
if (self.suspended) then
self.resume;
end;
procedure TMyJobProcessor.stop;
begin
self.terminate;
if (self.suspended) then
self.resume;
end;
procedure TMyJobProcessor.execute;
begin
repeat
if (fJobList.count>0) then
begin
// Get the next job from the list
fJobListCS.acquire;
try
fJob:=TMyJobDescription(fJobList[0]);
fJobList.delete(0);
finally
fJobListCS.release;
end;
// Create the results holding object
fJobResult:=TMyJobResult.create;
// Process the job here
// put the results in fJobResult
// Now, execute the callback with synchronize
synchronize(doCallback);
// Now free up the job description object
try
fJob.free;
except
end;
end
else
begin
if (not self.terminated) and (fJobList.count=0) then
self.suspend;
end;
until (self.terminated);
end;
As you can see, using 'synchronize' just requires us to 'pass' our parameters using private or protected variables. Now our job processor will play nice even if the call back accesses VCL objects or any other code that isn't thread safe (providing that other threads that access it also play by the rules and use synchronize).
Some objects provide their own means of handling multiple threads. TCanvas for example provides two methods. 'lock' and 'unlock'. So this is thread safe...
...
fACanvas.lock;
try
// Manipulate the canvas here
finally
fACanvas.unlock;
end;
...
But if in doubt, synchronize. You will of course have noticed in our revised job processing thread that I've replaced the 'workToDo' and 'getNextJob' functions with some real code which leads us on nicely to the next topic... resource protection.
Resource Protection
Resource protection is all about ensuring that two threads don't access the same resource at the same time. The implications of two threads accessing a TList for example could be quite bad. One checks for an item count... it finds the list has a single item so it reads it. Another thread checks for the item count from the same list... it also finds the list has a single item... in the meantime, the first thread deletes the object its just grabbed from the list. The second thread goes for it and consequently goes bang.
To avoid this kind of situation, various tools are provided... Synchronisation and object locking, we've already covered... that leaves '
TCriticalSection' and the nicely named '
TMultiReadExclusiveWriteSynchronizer'.
These two are essentially the same but with one subtle difference... the long one can help you avoid deadlock by allowing you to specify what kind of operation you are going to perform. But first, lets look at the short one... '
TCriticalSection'.
'
TCriticalSection' can be considered to be a token. Without a token, your thread can't enter the code it protects, and there is only a single token meaning that only a single thread can enter code protected by a critical section at any one time. In our revised job processing thread, we have these two blocks of code.
fJobListCS.acquire;
try
fJobList.add(aJob);
finally
fJobListCS.release;
end;
From the 'addJob' method, and...
fJobListCS.acquire;
try
fJob:=TMyJobDescription(fJobList[0]);
fJobList.delete(0);
finally
fJobListCS.release;
end;
From the 'execute' method. Its not a good idea to be adding and removing items from the same list at the same time, so we've protected the list with a critical section ('fJobListCS'). This is created in the constructor (fJobListCS:=
TCriticalSection.create) and destroyed in the destructor (fJobListCS.free). It is then used to ensure that only one thread can access the job list at once.
At this point, just in case its not clear... when you call a method of a thread object, even though the method belongs to a thread running its its own context, the method will be executed within the context of the calling thread. So whilst we will only ever remove jobs from the queue within the context of the job processing thread, any other thread (the main VCL thread included) that has access to the job processor can add a job.. thats why protecting the list when we add a job is so important.
Thankfully, using a critical section is pretty straight forward as you can see from this example. The methods we use are 'acquire' to obtain control of the critical section and 'release' to relinquish control.
Critical sections are not without their problems... they take time and can present a processing bottleneck, especially if you have alot of threads all trying to access common data through the same critical section. But these issues pale into insignificance when compared with deadlock. Deadlock occurs when two (or more) threads try to access the same critical section and for whatever reason, one thread is waiting for something else to happen before it releases the critical section for the others to use.
There are two key ways this can happen. The first is when you unexpectedly leave a block of code and fail to release the critical section. This will most likely be courtesy of an exception. For that reason you should ALWAYS use try...finally when using critical sections as illustrated in the example. Failure to do so could result in your thread retaining control of a critical section when it should have relinquished it.
The other way you can end up with deadlock is when you have multiple critical sections protecting different data sets.
procedure TMyThread1.execute;
begin
...
fGlobalDataQueueCS.acquire;
try
fGlobalResultBufferCS.acquire;
try
...
finally
fGlobalResultBufferCS.release;
end;
finally
fGlobalDataQueueCS.release;
end;
...
end;
procedure TMyThread2.execute;
begin
...
fGlobalResultBufferCS.acquire;
try
fGlobalDataQueueCS.acquire;
try
...
finally
fGlobalDataQueueCS.release;
end;
finally
fGlobalResultBufferCS.release;
end;
...
end;
In this example, fGlobalDataQueueCS and fGlobalResultBufferCS are property variables that hold references to two global critical sections. Thread 1 hits the first critical section which protects the global data queue... thread 2 on the other hand gets the critical section that protects the global result buffer. Thread 1 trys for the global result buffer critical section but of course thread 2 has it. Thread 2 trys for the global data queue critical section but thread 1 has it. Neither can progress because they are both trying for the critical section the other has.
To avoid this scenario, if you use multiple critical sections to protect different resources, always make sure that the critical sections get nested in the same order.
There are other ways to end up in a deadlock situation, but by thinking ahead and planning your threads and how they interact with common objects, you can practically reduce the chances of it happening to 0. To help make your life a little easy in this respect, we come to the aptly named '
TMultiReadExclusiveWriteSynchronizer'.
So what does such a nicely named object do... well, like a critical section, its purpose is to protect resources from simultaneous access by multiple threads... unlike a critical section however, it allows you to do that according to the type of operation you are going to perform.
If you have multiple threads and they all want to read the protected resource... no problem, they can all read it at the same time providing another thread isn't writing to it... if they want to write to it however, they will have to wait until everyone has finished reading and then they will have to take it in turns as only a single thread is allowed to write to the protected resource at once.
P
...
// Reading from the protected resource
fOurMultiReadExclusiveWriteSynchronizer.beginRead;
try
// Read from the protected resource
finally
fOurMultiReadExclusiveWriteSynchronizer.endRead;
end;
...
// Writing to the protected resource
fOurMultiReadExclusiveWriteSynchronizer.beginWrite;
try
// Write to the protected resource
finally
fOurMultiReadExclusiveWriteSynchronizer.endWrite;
end;
...
This allows you to reduce the bottlenecks that critical sections can introduce as you only ever restrict access to a single thread when you're writing to the resource.
Sample Job Processor
You can download a more complete job processor along with a simple demonstration
here.
This example code illustrates most of whats been covered and will provide you with a working example to experiment with. The sample is very simple... the jobs job is to simply wait for a period of time.
As always, you can use this code in your own projects so long as I get a little credit. Its functional and appeared to work well, but it was written specifically for this article so it hasn't been battle tested.
Conclusion
Hopefully, with this first installment, the world of multithreaded programming is now a bit clearer. When I first encountered it I actually couldn't believe how easy it was... and also how easy it was to completely mess up through a lack of understaning with regards to resource protection etc.
My first attempt was a business critical service for one of my employers... it worked great for the first few days... then it crashed. Another few days... *CRASH* Needless to say I didn't score any brownie points for that effort but since then, I've moved on a little. The browser based game I run has its processing service (the ticker). It uses multiple threads to get the best out of the dual CPU machine it runs on. Written as a Windows service, it has the service thread (equivalent to the main VCL thread) which spawns a management thread when it needs to get to work. This in turn spawns 4 different types of thread... one which prepares the database for the tick (one instance), then when that completes, the main workhorse thread (two instances) and finally a cleanup thread (one instance) and the score calculator (one instance). Why am I telling you this?
Well for two reasons...
Firstly, for a bit of moral support if you are struggling with a multithreaded application. My first attempt was a complete failure, but with a bit of perseverance, I've managed to improve my understanding of the subject quite substantially. So much so that now the only time my multithreaded server apps stop working is when the server is shutdown or rebooted. So, if you're having problems stick with it or ask for a bit of help.
And secondly, to try and illustrate whats possible with multithreading. Once you get your head around the potential pitfalls, you can do great things with it. As an example, if our ticker starts to struggle with its workload (if we get lots of players for example), the main workhorse thread has been designed such that it could be farmed out to other servers. The tick preparation thread includes a balancing phase that spreads the workload evenly between a specified number of threads. So... add another dual core server, tell the balancer to spread across 4 threads and then add some software synchronization to manage the two threads on the extra server and I have myself a processing farm.
Anyhow, this has turned out to be a much longer article than I planned so its time for me to go. I hope its made things clearer if you are new to multithreading, but just in case... if you have any questions or you spot an error drop me a mail on athena at outer hyphen reaches dot com and I'll do my best to help you out.
The next installment on multi-threading will cover debugging, throttling and some suggestions regarding tuning for single/multi core machines.
Thanks for reading.... until next time, take care and happy coding :-)