• Recent Tutorials

  • Tripping The Class Fantastic: Multi-threading Part 1

    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.

    Code:
    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;
      public
        constructor create;
        destructor destroy; override;
    
        procedure execute; 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...

    Code:
      ...
     
      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.

    Comments 6 Comments
    1. chronozphere's Avatar
      chronozphere -
      Fantastic articles athena. I will definitely read 'em all.

      Multithreading still scares me a bit and the only way to overcome that is by rolling up the sleeves haha... I would like to use them in my engine, to separate rendering and audio from the rest of the code.

      I really need to write an article too btw. There aren't that many and new site does a good job at presenting them.
    1. WILL's Avatar
      WILL -
      Quote Originally Posted by chronozphere View Post
      I really need to write an article too btw. There aren't that many and new site does a good job at presenting them.
      Feel free! We need a lot more beginner articles. Either an introduction to a specific API or a completely non-API specific article about the basics of game programming.
    1. AthenaOfDelphi's Avatar
      AthenaOfDelphi -
      Thanks

      It's not quite as scary as it may seem at first... although I speak from the point of view of someone who nearly got fired because the first time they used multi-threading was in a business critical application which kept crashing because I messed up

      After that, no programming problem has ever seemed quite so scary
    1. code_glitch's Avatar
      code_glitch -
      nice stuff. i have written one slightly less popular article as it would seem but am working on another as will would tell you with the newbie-frindly project or something
    1. Dan's Avatar
      Dan -
      AthenaOfDelphi,
      Do you have any comments on suspend and resume procedures being deprecated starting from D2009. Since you are using these methods in your examples it may confuse some people because stuff normally gets deprecated when better ways of doing things become available.
    1. AthenaOfDelphi's Avatar
      AthenaOfDelphi -
      Dan, apologies for the delay... well, what can I say... I didn't realise they were deprecated, certainly my compiler doesn't complain about anything (I get the feeling from reading one or two articles it should complain).

      Whilst I understand the theories I've read, I can honestly say I've (a) never used the suspend/resume mechanism for synchronising threads and (b) never had any problems using suspend/resume. I generally use it for job processing type scenarios where the thread put's itself to sleep until it is needed. Replacing these calls with an eventing mechanisms (wait_for_single_object IIRC) shouldn't be too much of a problem. Something like this:-

      Code:
      unit classSuspendableThread;
      
      interface
      
      uses
        sysUtils, classes, windows;
      
      type
        TSuspendableThread = class(TThread)
        protected
          fWakeUpEvent        : THandle;
          fSleeping           : boolean;
      
          procedure goToSleep;
        public
          constructor create(createSuspended:boolean);
          destructor Destroy; override;
      
          procedure terminate;
      
          procedure wakeUp;
      
          property sleeping:boolean read fSleeping;
        end;
      
      implementation
      
      constructor TSuspendableThread.create(createSuspended:boolean);
      var
        temp    : string;
        nameBuf : PWideChar;
      begin
        inherited create(createSuspended);
      
        temp:='WUE-'+intToHex(self.ThreadId,8);
        nameBuf:=strAlloc(length(temp));
        strPCopy(nameBuf,temp);
        fWakeupEvent:=createEvent(nil,false,false,nameBuf);
        strDispose(nameBuf);
      end;
      
      destructor TSuspendableThread.destroy;
      begin
        closeHandle(fWakeupEvent);
      
        inherited;
      end;
      
      procedure TSuspendableThread.terminate;
      begin
        TThread(self).terminate;
      
        if (fSleeping) then
        begin
          wakeup;
        end;
      end;
      
      procedure TSuspendableThread.wakeUp;
      begin
        if (fSleeping) then
        begin
          try
            setEvent(fWakeupEvent);
          except
          end;
        end;
      end;
      
      procedure TSuspendableThread.goToSleep;
      begin
        fSleeping:=true;
        waitForSingleObject(fWakeupEvent,INFINITE);
        fSleeping:=false;
      end;
      
      end.
      This class works, call 'wakeUp' instead of resume (although resume, or it's replacement start would still be needed if you created it suspended). You will of course note that the 'gotoSleep' method which replaces suspend is now protected. Calling it from anywhere other than the execute method will hold the calling thread.

      This solution is far from perfect. The Windows API calls keep a count of suspends and resumes which this class does not. I suspect there are numerous ways of solving the puzzle and your solution will depend very much on usage.

      Do I think deprecating them is a good idea... it would be a cold day in hell if I said yes. They are legitimate mechanisms for controlling threads (and as I say, ones I've used myself on numerous occasions with no problems). So, I've just gone looking for an answer. I think this excerpt sums it up nicely (taken from this article by Peter Richie):-

      For time eternal there has been a way to suspend (and by association resume) another thread. It wasn't until .NET 2.0 that someone took leadership and conceded that suspending another thread is not a safe thing to do. .NET 2.0 deprecates System.Threading.Thread.Suspend() and Resume() (although, Resume() isn't dangerous by association...).

      Basically, Suspend() has no care that the thread may actually be managing one or more complex invariants that can't be changed atomically. Normally with complex invariants synchronization is used to ensure while one thread is modifying the individual components of an invariant another thread doesn't try to access it. This is important because the validity of an invariant is rarely atomic; meaning changing an invariant may take several steps and may be in an unknown or invalid state between the first and the last step (a date is a good example, setting the day and month is two steps; until both steps are complete the date might be invalid). Suspending a thread circumvents the synchronization primitives and cuts the thread off at the knees.
      Basically, calling suspend from outside a thread is bad... well all I can say is "there's a suprise". I've never done it and I wouldn't encourage you to do it for the reasons Peter gives.... the caller of suspend has no idea about the status of the thread. So, maybe a better solution would to have been to change visibility of the suspend method so that it was only accessible to the thread itself.

      Who knows, but I certainly don't agree with removing them as it's going to mean we have some changes to do at work when we finally port to an up to date Delphi version.
Comodo SSL