Using Semaphores in Delphi, Part 1

By: Cary Jensen

Abstract: Semaphores are like mutexes on steroids. Not only can they coordinate multiple threads and process, but they can permit more than one simultaneous lock. This article shows you how to use these useful objects in a multithreaded environment

Semaphores are powerful synchronization objects that, like mutexes (mutually exclusives), permit you to coordinate multiple threads and processes in your Delphi applications. Likewise, in multithreaded server environments, such as WebSnap, Web Broker ISAPI servers, and IntraWeb applications, semaphores provide a flexible mechanism for protecting shared resources.

This article begins with brief introduction to synchronization. This is followed by a detailed look at using semaphores in your Delphi applications.

This article is part one of a two part series. In this article, the basic use of a semaphore is demonstrated. In part two, the use of a semaphore to implement a database connection pool is demonstrated.

Threads and Synchronization

Due to Delphi's TThread class, it is relatively easy to create multithreaded applications. You create a TThread descendant using the Thread Object Wizard in Delphi's Object Repository. You then implement this new class's Execute method, placing in it the code that you want executed in the thread.

If this process is so easy, you might ask, then why don't more Delphi developers make use of additional threads in their applications? When questioned about this, most developers point out that multithreaded applications are inherently more complex than single threaded applications.

In short, this complexity arises when two or more threads must access a common resource. Consider a single threaded application. When your code writes a value to a variable, and then, some time later, uses the value stored in that variable as a parameter to a procedure, you can be sure that the value in the variable is the very same value that was previously written.

This seems reasonable enough. However, if the application is multithreaded, and more than one thread can write a value to that variable, the assumption that a value assigned to a variable by code at one moment will be the value contained in that variable a little later is a dangerous one. In other words, some basic assumptions that are valid in a single threaded environment can lead to serious problems in a multithreaded environment.

Here is another example. Imagine that you write a function that opens a file handle, writes some data to the file, and then closes the file handle. In a single-threaded environment, calling this function will result in the writing of data to the file, and nothing else. In other words, between the initial invocation of the function and its return, the only operations that the application will perform are related to the writing of the data to the file.

Once again, in a multithreaded environment you cannot assume that between the invocation of a function and its return that only the function's code is executed by your application. It is not only possible, but likely, that between the invocation of a function and its return, one or more other threads within the application may perform tasks. And unless you take precautions, it is possible that these other threads may interfere with the thread that is trying to write data to the file, either by compromising the data that is being written, the handle being used for the file, or by changing some other shared resource.

In a multithreaded environment it is important to remember that an individual thread has no control over when it will execute. In the pre-emptive multitasking environment of 32-bit Windows, it is the operating system that does this. Furthermore, the operating system has no notion of a unit of work being performed by a thread. If you need to ensure that a thread completes a unit of work before another thread that shares a common resource runs, you must take explicit steps to block any potentially disruptive threads until the unit of work is completed. This process is called synchronization.

For the most part, as you become acquainted with how threads work, and consider what units of works must not be interrupted by other threads, and learn to use appropriate technique to protect those units of work, you will find that multithreaded programming is really not much more complicated than single-threaded programming. In the end, its a matter of mindset. Like with most other problems in application development, once you know what to lookout for, and how to protect against potential problems, everything else will fall into place.

Understanding Semaphores

A semaphore, like a mutex, is an operating system-level object that you can use to synchronize your code. Like mutexes, semaphores can be used between threads in a single process as well as across processes. In fact, a semaphore that supports a maximum count of one serves essentially the same purpose as a mutex.

There is one very big difference between semaphores and mutexes, however. While a mutex can be acquired by no more than one thread or process at a time, semaphores can be designed to allow two or more threads to acquire the semaphore simultaneously.

This might not make sense at first. Specifically, synchronization is normally used to ensure that one and only one thread can obtain the synchronization object, thereby preventing its unit of work from being interrupted by another thread that shares some of the same resources. Doesn't permitting two or more threads to access a common resource introduce instability?

The answer is no, so long as what is being synchronized by the semaphore can be used by two or more threads simultaneously. For example, consider a database connection pool. A database connection pool provides a set of database connections that can be shared by a population of threads. A semaphore is an ideal object for controlling access to a connection pool.

Here is how it works. Any time a thread needs to get a connection from the connection pool, it calls one of the Windows wait functions, passing the semaphore's handle to the function. If a connection is available, the wait returns, and the semaphore's available count is decremented. When the thread no longer needs the connection, it releases the semaphore, and the semaphore's lock count is increased again.

So long as the configured maximum count of the semaphore is equal to or less than the number of available connections, the semaphore object ensures that any thread requesting a connection will be blocked if all connections are currently in use. A blocked thread will continue to be blocked until either a connection becomes available, or a timeout period expires.

You create a semaphore using the Windows API function CreateSemaphore. The following is the syntax of this function:

function CreateSemaphore(lpSemaphoreAttributes: PSecurityAttributes; 
  lInitialCount: Integer; lMaximumCount: Integer ;
  lpName: PAnsiChar): Caradinal;

When you call CreateSemaphore, the first parameter that you pass is a pointer to a security attributes structure. To use the default security attributes, pass a value of nil.

The second parameter defines the initial count of the newly created semaphore, and the third parameter sets the maximum count. The second parameter must be a value between zero and the maximum count, and the third parameter must be greater than 1. If you pass a value of one in both the second and third parameters, the semaphore has one, and only one available lock (and it is not yet locked). The first thread to call a wait function for this semaphore will obtain the lock.

It is pretty common to set the second and third parameters to the same value. Doing so creates a semaphore will all of its locks currently available.

The fourth parameter is the name of the semaphore. The name is required if you want to be able to get a handle to the semaphore created in one process from a thread in another process. If you do not need to open this semaphore from another process, you can pass an empty string in this parameter.

Just as you can do with a mutex, you can attempt to get the handle of an existing semaphore by calling OpenSemaphore. This function has the following syntax:

function OpenSemaphore(dwDesiredAccess: Cardinal; bInheritHandle: LongBool;
  lpName: PAnsiChar): Cardinal;

When calling OpenSemaphore, you use the first parameter to identify the type of access that you want for the semaphore. Most developers pass the value SEMAPHORE_ALL_ACCESS. Use the second parameter to identify whether or not a process created by this process can inherit the handle to the mutex. If you pass True, a process created through a call to CreateProcess can inherit the mutex handle, otherwise it cannot.

The third parameter is the name of the semaphore. As is the case with mutexes, this name must be unique.

If OpenSemaphore is successful, it returns the handle of the semaphore. If OpenSemaphore fails, it returns 0. If OpenSemaphore returns 0, use GetLastError to determine the cause of the failure.

As mentioned earlier, a thread wishing to acquire the semaphore does so by calling one of the Windows wait functions. These functions include WaitForSingleObject, WaitForMultipleObjects, WaitForSingleObjectEx, WaitForMultipleObjectsEx, SignalObjectAndWait, MsgWaitForSingleObjects, and MsgWaitForMultipleObjectsEx.

The example project created later in this article makes use of WaitForSingleObject. This function has the following syntax:

function WaitForSingleObject(hHandle: Cardinal; 
  dwMilliseconds: Cardinal): Cardinal;

When calling WaitForSingleObject, the first parameter that you pass is the handle of the semaphore that you obtained through a call to CreateSemaphore or OpenSemaphore, and the second parameter is the amount of time, in milliseconds, that you are willing to wait. If you never want the wait to timeout, pass the constant INFINITE in the second parameter.

If the semaphore has at least one count available, WaitForSingleObject succeeds immediately. In this case, the count of the semaphore is decremented by one, and WaitForSingleObject returns a value equal to the constant WAIT_OBJECT_0. 

If the semaphore has no counts available, WaitForSingleObject will block the current thread until either a count becomes available, or the wait times out, which ever comes first. If a count became available, the count of the semaphore is again reduced by one, and WaitForSingleObject returns WAIT_OBJECT_0. If the wait timed out, WaitForSingleObject returns WAIT_TIMEOUT.

Because a successful call to one of the Wait functions decrements the count of a semaphore, it is important for a thread to release the semaphore as soon as it is through working with the shared resource. When a thread releases a semaphore, the semaphore's count increments by one or more, thereby making the semaphore available to any other threads that are currently waiting for it.

You release a semaphore by calling ReleaseSemaphore. This function has the following syntax:

function ReleaseSemaphore(hSemaphore: Cardinal; lReleaseCount: Integer
  lpPreviousCount: Pointer): LongBool;

The first argument is the handle to the semaphore, and the second is the number of counts to add to the semaphore. A thread should only increment the semaphore's count equal to the number of counts held by that thread. Normally, this will be equal to one.

The third parameter is a pointer to 32-bit variable that can be assigned the count of the semaphore prior to the release. If you do not need to know the semaphore's previous count, pass nil in this third parameter.

All of the semaphore related functions and constants discussed in this section are declared in the Windows unit. Consequently, this unit must appear in the uses clause of any code that needs to use semaphores.

A Simple Semaphore Example

The following steps demonstrate how to create a simple project that demonstrates the use of a semaphore that can be locked multiple times.

  1. Create a new project. Place on the main form a Panel and a ListBox from the Standard page of the component palette. Set the Align property of the Panel to alLeft.
  2. Next place a Splitter (from the Advanced page of the Component palette). Its Align property should default to alLeft. Finally, set the ListBox's Align property to alClient.
  3. Place two Labels, two Edits, and two Buttons in the Panel. Set the Caption property of Button1 to Create Thread, and the Caption of Button2 to Clear ListBox.
  4. Set the Caption of Label1 to Post Write Sleep (milliseconds), and the Label2 Caption to WaitFor Duration (milliseconds).
  5. Set the Text property Edit1 to 2000, and the Text property of Edit2 to 1000. Finally, adjust the position and size of the components to look something like that shown in the following figure.


  6. Select File | New | Other, and double-click the Thread Object Wizard on the New page of the Object Repository. Set Class Name to TGetSemWriteAndLeave.
  7. Add both the Windows unit and the SysUtils unit to the interface section uses clause of the thread's unit.
  8. Modify the TGetSemWriteAndLeave class declaration to look like the following.
    type
      TGetSemWriteAndLeave = class(TThread)
      private
        { Private declarations }
        FSleepDuration: Integer;
        FWaitForDuration: Integer;
      protected
        WriteText: String;
        procedure Execute; override;
        procedure UpdateListBox;
      public
        property SleepDuration: Integer write FSleepDuration;
        property WaitForDuration: Integer write FWaitForDuration;
      end;
  9. With your cursor on the class declaration in the editor, cress Ctrl-Shift-C to use class completion to generate the UpdateListBox method block in the implementation section.
  10. Select File | Use Unit, and select the main form's unit to add it to the thread unit's uses clause.
  11. Modify the UpdateListBox and Execute methods of the thread to look like the following:

    procedure   TGetSemWriteAndLeave.Execute;
    var
      WaitResult: Integer;
    begin
     WaitResult := WaitForSingleObject(Semaphore, FWaitForDuration);
      case WaitResult of
       WAIT_OBJECT_0: WriteText := 'Got it. Thread ID: '  
         + IntToStr(Self.ThreadID);
       WAIT_TIMEOUT: WriteText := 'Timed out. Thread ID: '  
         + IntToStr(Self.ThreadID);
       WAIT_ABANDONED: WriteText := 'Other. Thread ID: '  
         + IntToStr(Self.ThreadID);
      end;
     Synchronize(UpdateListBox);
     sleep(FSleepDuration); //wait until releasing semaphore
     ReleaseSemaphore(Semaphore, 1, nil);
    end;

    procedure   TGetSemWriteAndLeave.UpdateListBox;
    begin
      Form1.ListBox1.Items.Add(WriteText);
    end;
  12. Move to the main form's unit. Update the interface var block to look like the following:

    var
      Form1: TForm1;
      Semaphore: THandle;
  13. Select File | Use Unit from Delphi's main menu, and select the thread's unit.
  14. Select the button labeled Create Thread and add the following OnClick event handler:

    procedure
    TForm1.Button1Click(Sender: TObject);
    begin
      with TGetSemWriteAndLeave.Create(True) do
      begin
       FreeOnTerminate := True;
       SleepDuration := StrToInt(Edit1.Text);
       WaitForDuration := StrToInt(Edit2.Text);
       Resume;
      end;
    end;
  15. Select the button labeled Clear ListBox and add the following OnClick event handler:

    procedure TForm1.Button2Click(Sender: TObject);
    begin
      ListBox1.Items.Clear;
    end;
  16. Finally, add the following initialization and finalization sections to the end of the main form's unit, immediately prior to end.:

    initialization
      Semaphore := CreateSemaphore(nil, 3,3,'');

    finalization
      CloseHandle(Semaphore);
  17. Run the project. Click the Create Thread button many times very quickly. After a short time, the list box will begin to display the messages from the threads, some of which indicate that they timed out waiting for the semaphore, as shown in the following figure. 

As you can see from this project, while many of the threads where able to access the semaphore, some timed out before the semaphore was acquired. You might want to try this project with a variety of different post write sleep and waitfor duration values.

This completed project can be downloaded from Code Central by clicking here.

In part 2 of this series, the use of a semaphore to implement a database connection pooling object is demonstrated.

This series was adapted from Mastering Multithreading and Other Advanced Delphi Topics by Cary Jensen, one of the Delphi Developer Days 2003 Power Workshops, focused Delphi (TM) training. For information on this an other Delphi Developer Days 2003 Power Workshops, visit http://www.DelphiDeveloperDays.com.

About the Author

Cary Jensen is President of Jensen Data Systems, Inc., a Texas-based training and consulting company that won the 2002 Delphi Informant Magazine Readers Choice award for Best Training. He is the author and presenter for Delphi Developer Days (www.DelphiDeveloperDays.com), an information-packed Delphi (TM) seminar series that tours North America and Europe, and Delphi Developer Days Power Workshops, focused Delphi (TM) training. Cary is also an award-winning, best-selling co-author of eighteen books, including Building Kylix Applications (2001, Osborne/McGraw-Hill), Oracle JDeveloper (1999, Oracle Press), JBuilder Essentials (1998, Osborne/McGraw-Hill), and Delphi In Depth (1996, Osborne/McGraw-Hill). For information about onsite training and consulting you can contact Cary at cjensen@jensendatasystems.com, or visit his Web site at www.JensenDataSystems.com.

Click here for the current schedule of Delphi Developer Days Power Workshops, focused Delphi (TM) training.

New!: Stay informed, stay in touch. Register online to receive the free Developer Days ELetter: information, observations, and events for the Delphi developer by Cary Jensen. Each Developer Days ELetter includes Delphi tips and tricks, .NET information, links to recent articles posted to the Borland Developers Network site, and events in the Delphi community. Click here to sign up now

   

Copyright ) 2003 Cary Jensen, Jensen Data Systems, Inc.
ALL RIGHTS RESERVED. NO PART OF THIS DOCUMENT CAN BE COPIED IN ANY FORM WITHOUT THE EXPRESS, WRITTEN CONSENT OF THE AUTHOR.


Server Response from: ETNASC03