Working with TMultiReadExclusiveWriteSynchronizer

By: Justin Swett

Abstract: Using TMultiReadExclusiveWriteSynchronizer in Multi Thread Applications

Working with TMultiReadExclusiveWriteSynchronizer
By Justin Swett
Delphi Developer Support

      There will undoubtedly come a time when the need for developing a multi threaded application is a necessary. In some cases the use of a TMultiReadExclusiveWriteSynchronizer (TMREWS for short) will improve the performance of your threads, and therefore improve the performance of your application. (As a parenthetical side note, don't you just love saying "TMultiReadExclusiveWriteSynchronizer!") This article will cover the basics of using a TMREWS and point out some not so obvious pitfalls to avoid. You can download the complete project here.


When to use a TMultiReadExclusiveWriteSynchronizer
As the name of the component implies, a TMREWS should be used when there is much reading and little writing to be done. In addition, the READ operations should be kept to a MINIMUM otherwise your write threads could be left waiting indefinitely. If there are going to be equal amounts of READING and WRITING then you might want to consider using critical sections instead.

Creating a Thread Class that uses the TMREWS
One of the first things to do is declare a TMREWS. If you downloaded the complete project, you'll see a global lock declared inside the implementation section of the thread unit. It doesn't matter where the lock is declared so long as the following criteria are met:
  • Threads can access the lock.
  • The lock has been instantiated.
    (Use initialization section and finalization for this)
  • Threads can access memory that you are trying to read/write to.
Once the lock has been declared you can create your thread class. You will need to override the Execute procedure, which is where you will be using TMREWS lock. Here is an example of an execute procedure using the TMREWS:

procedure TWorkerThread.Execute;
begin
  { Place thread code here }
  while not Terminated do
  begin
    //Updates form that thread is "sleeping"
    Synchronize(Sleeping);
    //Sleep thread momentarily, for illustrative purposes
    Pause;  

    //Update form that thread is "waiting" 
    Synchronize(Waiting);

    //check to make sure thread hasn't been terminated
    if Terminated then exit;

    //if reader then
    if (fThreadType = ttRead) and (not Terminated) then 
    begin
      globalLock.BeginRead;
      //wrap endRead in try finally... otherwise deadlock could occur
      try
        Synchronize(Reading);    //Still important to synchronize VCL calls
        Pause;                   //represents thread work being done  
        Synchronize(DoneReading);
      finally
      globalLock.EndRead;
      end;
    end
    //if writer then
    else if (fThreadType = ttWrite) and (not Terminated) then 
    begin
      globalLock.BeginWrite;
      //wrap in a try finally to prevent deadlock
      try
        Synchronize(Writing);
        Pause; // represents data manipulation
        Synchronize(DoneWriting);
      finally
      globalLock.EndWrite;
    end
    else
      exit;
  end;
end;
Notice that the work done after Begin'X' is wrapped in a try finally. This ensures that that the appropriate End'X' will get called if any exceptions are raised . If an exception is raised then other threads might not be able obtain locks. If there is no chance of an exception being raised, then removing the try finally for performance reasons would be acceptable.

Make sure any memory being referenced by your threads is sandwiched with calls to the TMREWS BeginREAD/WRITE and EndREAD/WRITE, otherwise your work of making your data thread safe is forfeited.

"Lock it up Chris, I'm about to..."
TMREWS allows you to promote a READ lock to a WRITE lock if the same thread were to make such a request. Although this can be advantageous it can also cause two threads to lock up on each other. For example, two instances of a thread type are instantiated with and Execute method which promotes a read lock to a write lock. If the first thread calls BeginWrite after the second thread has called BeginWrite then the first thread is blocked. The second thread will at some point attempt to call BeginWrite, which will also get blocked. Now the two threads are waiting for the other to release the lock.

Another situation will arise where several Read threads are running and perhaps just a few Write threads. When ever a Read thread calls BeginRead a Read lock is granted... so long as there isn't a Write lock currently out. If a Write thread were to call BeginWrite while a Read thread had the lock it could potentially be wait indefinitely, while other read threads obtain Read locks. Requests for locks are not queued, so this isn't a bug and is in fact as designed.

Summary
Using the TMultiReadExclusiveWriteSynchronizer (just had to say it one more time) is fairly simple. To use them successfully you should call BeginRead/Write matched with the appropriate EndRead/Write. You should also check you read locks out for as little time as possible if you want your write threads to complete their execution.

Server Response from: ETNASC03