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.
- 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.
- 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.
- 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.
- Set the Caption of Label1 to Post Write Sleep (milliseconds), and the Label2
Caption to WaitFor Duration (milliseconds).
- 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.

- Select File | New | Other, and double-click the Thread Object Wizard on
the New page of the Object Repository. Set Class Name to TGetSemWriteAndLeave.
- Add both the Windows unit and the SysUtils unit to the interface section uses clause of the thread's unit.
- 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;
- 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.
- Select File | Use Unit, and select the main form's unit to add it to the
thread unit's uses clause.
- 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;
- Move to the main form's unit. Update the interface var block to look like
the following:
var
Form1: TForm1;
Semaphore: THandle;
- Select File | Use Unit from Delphi's main menu, and select the thread's unit.
- 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;
- Select the button labeled Clear ListBox and add the following OnClick
event handler:
procedure TForm1.Button2Click(Sender: TObject);
begin
ListBox1.Items.Clear;
end;
- 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);
- 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.