Creating Synchronization IPC Classes with Kylix

Rick RossPILLAR Technology Group, Inc.


Introduction

Key Concepts

Synchronization

Task

Processes

Threads

GNU C Library- glibc

Synchronization Objects available in Linux

Mutexes

Condition Variables

POSIX Semaphores

System V Semaphores

Read-Write Locks

Record Locking

Creating a Cross-Platform Mutex Class

Class Definition

Constructors

Destructors

Locking Methods

Unlocking Methods

Typical Mutex Usage

Creating a Condition Variable Class

Class Definition

Constructor

Destructor

Waiting

Signaling and Broadcasting

Typical Usage – Signaling/Broadcasting

Typical Usage – Waiting

Creating a POSIX Semaphore Class

Class Definition

Constructor

Destructor

Waiting

Posting

Current Value

Using the Classes

Mutex Example: A File Logging Class

Condition Variable Example: A Thread-Safe Queue

POSIX Semaphore Example: A Double Buffered File Copier

Additional Resources


Introduction

The purpose of this paper is to create Synchronization Interprocess Communication (IPC) classes with Kylix. In this session, you will learn what synchronization methods are available in Linux, what purpose each is best used for, creating reusable classes and identify some cross-platform design issues.

In order to avoid the phrase ‘multiple processes and/or threads’, I will simplify the phrase to ‘multiple threads’. Unless I specifically say that a certain topic applies only to multiple threads, you can assume that it also applies to multiple processes. I will point out differences whenever they are applicable.

Key Concepts

Listed below are several key concepts that need to be understood before diving into the topic at hand. Because these concepts are woven into this entire paper, it is essential that these concepts are clearly understood.

Synchronization

Synchronization allows multiple threads to coordinate the usage of shared resources. They prevents multiple threads from accessing the same resource at the same time. Linux provides the low-level APIs, and Kylix provides a method for creating easy to use classes. Once these classes have encapsulated the IPC functions, more time can be spent on coding the actual solution, rather than the underlying plumbing that is needed to make it all work.

Task

A task is a unit of work that is scheduled by the Linux scheduler. It includes both processes and threads.

Processes

Processes have existed from the beginning of the programming era. A process is an executable program that is currently loaded. It can either be running, stopped or blocked. Stopped processes are those are currently being debugged or have received a SIGSTOP, SIGSTP, SIGTTIN, or a SIGTTOU signal. Blocked processes are those that are waiting to be executed. Each process has it’s own instruction pointer, address space, memory space, and other various CPU registers.

The instruction pointer is used to keep track of what the current instruction the program is running. Address space (also known as the text segment) refers to the memory location of where the process’ program code is loaded. Memory space refers to the heap (where memory is dynamically allocated), the stack (where local variables are located) and the global process data.

Without using an IPC of some kind, a process cannot communicate with another process. The good news is a process cannot directly read or write another process’ memory. This is good news because a process cannot inadvertently corrupt another process. Imagine if a program could write data into another process. The results could be disastrous. At the same time, the bad news is a process cannot directly read or write another process’ memory. When processes need to communicate, they need to use an IPC like pipes or shared memory. Coordinating and using any IPC mechanism requires diligent design and adds additional overhead.

New processes are created by using one of the exec APIs: execl, execlp, execle, execv, and execvp. The definition of these APIs can be found in Libc.pas.

Threads

A thread is path of execution in a process. In a normal process, there is only one path of execution – a single threaded program. Each thread has its own instruction pointer and various CPU registers. It shares the same memory space with the process that created the thread. All of the threads in the process share the same global memory, but each thread is allocated a chunk of memory for its stack space.

Threads are sometimes referred to as “Lightweight Processes”. In Linux, they are scheduled in the kernel. At the kernel level, threads are viewed as processes that share the same global memory. This makes it much easier for the kernel to schedule tasks. However, the kernel does not know that a process is a collection of threads.

Linux does not address multi-threaded applications in the same way as other environments. It does not recognize a particular application boundary for a collection of related threads; rather it treats threads and processes similarly. For most applications this myopia is fine. When dealing with multi-processor computers, however, the scheduling module that is currently implemented does not allow for applications to take advantage of the additional processors. The reason for this is that the kernel has no association between two tasks of the same process.

The system call that is usually used to create threads is the pthread_create function. Another API that can be used is the clone function, but it is more complex to use and is not as portable as the pthread_create function. In fact, pthread_create uses the clone function to create a thread. Both functions are located in Libc.pas. Kylix has made it even easier to use by developing the abstract TThread class located in Classes.pas. In addition, applications that descend from the TThread class are cross-platform, as long as they do not use platform specific code.

In Linux, threads can be attached or detached. Use attached threads when they need to be controlled. Valid reasons for using attached threads are: waiting for the thread to finish, the need to get a return value from the thread or to cancel a thread. When a thread is independent, use detached threads. In the current implementation of TThread object, threads are created as attached and are detached, using the pthread_detach function, just before the thread is finished.

GNU C Library (glibc)

The GNU C Library is where certain system calls are found in Linux. Memory allocation, file handling, thread creation, and many other useful functions are all part of the GNU C Library. In Kylix, these routines have been defined in Libc.pas.

Synchronization Objects available in Linux

Linux has a number of options for controlling the synchronization between multiple threads. Synchronization objects (which are part of glibc) can be further broken down into locking and waiting categories. A locking mechanism is used to protect multiple accesses to a specific resource, while a waiting mechanism waits for some event or condition to be satisfied. The available synchronization objects are: Mutexes, Condition Variables, POSIX Semaphores, System V Semaphores, Read-Write Locks and Record Locking.

Mutexes

Mutex is an abbreviation for mutually exclusive. Only one owner can lock a resource at a time. Its job is to prevent multiple accesses to a protected resource.

The following table shows the functions that initialize, lock, unlock and destroy mutexes:

Function

Description

pthread_mutex_init

Initializes the mutex with specific attributes

pthread_mutex_destroy

Destroys the mutex

pthread_mutex_trylock

Attempts to lock the mutex if it can. Will not block if unable to lock.

pthread_mutex_lock

Locks the mutex. Will block until it can lock it.

pthread_mutex_timedlock

Attempts to lock a mutex within a given timeframe

pthread_mutex_unlock

Releases a lock on the mutex

All of these functions take a parameter of type TRTLCriticalSection, which is a type definition of pthread_mutex_t, the actual mutex glibc object.

Mutex Attributes

Mutex attributes allow for changing the default behavior of mutexes. For instance, when a mutex needs to be shared between multiple processes, set the PTHREAD_PROCESS_SHARED flag. The default behavior is specified with the PTHREAD_PROCESS_PRIVATE flag, which does not allow the mutex to be shared between processes.

The following table shows the functions that manipulate mutex attributes:

Mutex Attribute Functions

Description

pthread_mutexattr_init

Initializes the mutex attribute

pthread_mutexattr_destroy

Destroys the mutex attribute

pthread_mutexattr_getpshared

Retrieves the current value of the processed shared flag

pthread_mutexattr_setpshared

Sets the processed shared flag

pthread_mutexattr_gettype

Retrieves the type of the mutex

pthread_mutexattr_settype

Sets the type of mutex

 

 Mutex Types

Mutex types can be either normal (or also known as default), recursive, or error checking. A normal mutex will not detect that the same thread has already locked a mutex. It will block indefinitely and cause a deadlock. A recursive mutex will allow a thread to repeatedly lock a mutex that it currently owns. However, care must be made to make sure that the mutex is unlocked the same number of times that it was locked. An error checking mutex validates which thread is making the calls. For example, if thread A has locked a mutex and thread B tries to unlock the same mutex, an error will be returned. Similarly, if thread A has locked a mutex and it tries to lock the same mutex again, it will also generate an error.

Mutexes are for locking shared resources. Remember, when using mutexes, any time that access to the same data needs to be exclusive, it must be protected by the same mutex.

Condition Variables

Condition variables are used when waiting for a programmer-defined condition to occur. While a locking mechanism (like a mutex) can be coded to wait for a condition, the resulting code is very inefficient. Using condition variables allows Linux to put the process or thread to sleep until the condition has occurred.

The following table shows the functions that initialize, signal, broadcast, waiting and destroy condition variables.

Function

Description

pthread_cond_init

Initializes condition variable with specific attributes

pthread_cond_destroy

Destroys the condition variable

pthread_cond_signal

Signals one waiting thread that the condition has been satisfied

pthread_cond_broadcast

Broadcasts to all waiting threads that the condition has been satisfied

pthread_cond_wait

Waits for the condition to occur.

pthread_cond_timedwait

Waits for the condition to occur within a given timeframe

 All of these functions take a parameter of type TCondVar, which is a type definition of pthread_cond_t, the actual glibc object for a condition variable.

All condition variables have an associated mutex. This mutex is used to lock out other threads while the condition is being modified. In particular, the pthread_cond_wait and pthread_cond_timedwait, must own the associated mutex before calling these functions. Once the wait functions are called, the associated mutex is released, and the thread waits for the condition to be signaled or broadcasted. Upon receiving the notification, it will then lock the mutex again, before returning.

Condition Variable Attributes

Condition variable attributes are similar to the mutex attributes. They specify if the condition variable can be shared between processes. For instance, when a condition variable needs to be shared between multiple processes, set the PTHREAD_PROCESS_SHARED flag. The default behavior is specified with the PTHREAD_PROCESS_PRIVATE flag, which does not allow the condition variable to be shared between processes.

The following table shows the functions that manipulate condition variable attributes:

Condition Variable Attribute Functions

Description

pthread_condattr_init

Initializes the condition variable attribute

pthread_condattr_destroy

Destroys the condition variable attribute

pthread_condattr_getpshared

Retrieves the current value of the processed shared flag

pthread_condattr_setpshared

Sets the processed shared flag

 

Condition variables are for waiting for a programmer-defined event to occur. They have a mutex associated with them and can signal one thread, or broadcast to all waiting threads.

POSIX Semaphores

A semaphore can be used for both locking and waiting. At the heart of a semaphore is an integer counter. This counter controls access to the protected resource. When creating a semaphore, the initial value of the count must be specified. A positive count indicates that the resource is available, while a count less than or equal to zero indicates that the resource is not available, and will block the calling process or thread.

Semaphores have two operations: they are either waiting or posting. Waiting checks the value of the count and if it is positive, it will decrement the value. Posting to a semaphore increments the value of the count and wakes any waiting processes or threads.

Mutexes can be emulated by a using a binary semaphore. A value of one indicates that the resource can be accessed, and zero indicates that the resource is locked. Remember that mutexes must be unlocked by the same process or thread that locked it. Semaphores, on the other hand, can be unlocked by any process or thread.

When it comes to waiting, semaphores are similar to condition variables. The important difference is that semaphores do not have a broadcast mechanism, whereas condition variables do. Another important distinction is that semaphore posting is always remembered. A signal or broadcast from a condition variable can be lost if there are no waiting threads or processes at the time of the signal.

The final distinction is that semaphores are the only synchronization object that can be called from a signal handler.

There are two types of POSIX Semaphores: named and memory based (also known as unnamed). A named semaphore has a POSIX IPC name associated with it.

The following table shows the functions that initialize, open, wait, post, close, destroy, and delete POSIX Semaphores:

POSIX Semaphore functions

Description

sem_init

Initializes a memory-based semaphore

sem_open

Opens a named semaphore (currently not implemented in Linux)

sem_wait

Waits until the semaphore count is greater than zero.

sem_timedwait

Waits until the semaphore count is greater than zero or until a specified time

sem_trywait

Tries to obtain the semaphore if it is available (does not block)

sem_post

Posts a semaphore (increments the count)

sem_getvalue

Retrieves the current count of the semaphore

sem_close

Closes the specified named semaphore

sem_unlink

Removes the specified named semaphore from the system

sem_destroy

Destroys the specified memory-based semaphore

POSIX Semaphores can be used to lock resources as well as wait for programmer defined conditions to occur. They are also safe to use in signal handlers to post to waiting threads. Finally, they can be unlocked by any process or thread.

System V Semaphores

System V Semaphores are a set of one or more semaphores. They are functionally similar to POSIX named semaphores.

The following table lists the System V Semaphore functions:

System V Semaphore functions

 Description

semctl

Performs control operations on a single semaphore.

semget

Creates or opens an existing set of semaphores

semop

Operates on a set of semaphores

System V Semaphores have been listed for completeness only and will not be discussed any further. 

Read-Write Locks

Read-Write locks are another locking synchronization object. They distinguish between a reading lock and writing lock. While a read lock is placed, only reading can take place, allowing for multiple readers. A write lock can only be obtained when there are no readers. Read-Write locks are memory-based.

The following table lists the Read-Write functions:

Read-Write functions

Description

pthread_rwlock_init

Initializes a read-write lock

pthread_rwlock_destroy

Destroys a read-write lock

pthread_rwlock_rdlock

Obtains a read lock

pthread_rwlock_tryrdlock

Tries to obtain a read lock if it is available (does not block)

pthread_rwlock_timedrdlock

Tries to obtain a read lock before the specified time

pthread_rwlock_wrlock

Obtains a write lock

pthread_rwlock_trywrlock

Tries to obtain a write lock if it is available (does not block)

pthread_rwlock_timedwrlock

Tries to obtain a write lock before the specified time

pthread_rwlock_unlock

Releases the lock on the read-write lock

Read-Write locks have been listed for completeness only and will not be discussed any further.

Record Locking

Record locking is used to share reading and writing of a file. Using the fcntl function performs all record locking operations. Record Locking has been listed for completeness only and will not be discussed any further.

Creating a Cross-Platform Mutex Class

Mutexes can be found in Linux and Windows NT as well as other operating systems. In this section, we will develop a cross-platform Mutex class that will work in Linux and Windows 95/98/NT and 2000. Designing cross-platform classes requires a least common denominator approach. Certain advanced functionality needs to be sacrificed in order to gain portable code. The Mutex class presented below is no exception. Under Linux, the timed lock functionality, and the process shared flag have not been implemented. For the Windows version, named mutexes and security attributes have not been implemented. By creating a descendant class for each platform, the functionality could be easily added while sacrificing portability.

Note: All of the classes are descendants of class called TIPCBase. This class contains debugging and helper methods for reporting errors.

Class Definition

TMtxAttribute = ( maDefault,      // default mutex atributes 
                  maFastNP,       // want a "fast" mutex (not portable)
                  maRecursiveNP); // want a Recursive mutex (not portable)

TMutex = class(TIPCBase)
private
  FMtx  : TRTLCriticalSection;
  FAttr : TMtxAttribute;
public
  constructor Create(attr : TMtxAttribute);
  destructor  Destroy; override;
  procedure Lock;
  procedure UnLock;
  function TryLock : boolean;
end;

Constructor – Linux Version

In the constructor of the Mutex class for Linux, the attribute structure is initialized first, then assigned the corresponding attribute. Finally the mutex is initialized. Any errors will raise an exception.

constructor TMutex.Create(attr : TMtxAttribute);
var
  mutexattr : TMutexAttribute;
  ret, attrvalue : integer;

begin   
  inherited Create;
  FAttr := attr;

  // Initialize the attributes   
  ret := pthread_mutexattr_init(mutexattr);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('pthread_mutexattr_init');

  case attr of 
    maFastNP      : attrvalue := PTHREAD_MUTEX_FAST_NP;
    maRecursiveNP : attrvalue := PTHREAD_MUTEX_RECURSIVE_NP;
  else      
    attrvalue := PTHREAD_MUTEX_DEFAULT;   
  end;

  ret := pthread_mutexattr_settype(mutexattr, attrvalue);   
  if ret <> LIBC_SUCCESS then      
    ErrorMessage('pthread_mutexattr_setttype');

  if pthread_mutex_init(FMtx,mutexattr) <> LIBC_SUCCESS then      
    ErrorMessage('pthread_mutex_init');
end;

 

Constructor – Windows Version

For Windows, the constructor creates the mutex with no security attributes, no name and does not initially lock the mutex. Any errors generate exceptions.

constructor TMutex.Create(attr : TMtxAttribute);
begin
  inherited Create;
  FAttr := attr;
  // For Win32 mutexes, we will not worry about security, name, 
  // or initially owning the mutex
  // Also attr is ignored
  FMtx := CreateMutex(nil,False,nil);
  if FMtx = 0 then
    ErrorMessage('CreateMutex');
end;

 

Destructor – Linux Version

The destructor simply destroys the mutex.

destructor TMutex.Destroy;
begin
  if pthread_mutex_destroy(FMtx) <> LIBC_SUCCESS then
    ErrorMessage('pthread_mutex_destroy');

  inherited;
end;

 

Destructor – Windows Version

The destructor closes the mutex.

destructor TMutex.Destroy;
begin
  if not CloseHandle(FMtx) then
    ErrorMessage('CloseHandle');
  inherited;
end;

 

Locking the Mutex – Linux Version

Locking is accomplished by using the Lock method, which will block until the mutex is available, or attempting to lock, without blocking. Both methods call the appropriate mutex API.

procedure TMutex.Lock;
var
  ret : integer;

begin  ret := pthread_mutex_lock(FMtx);
  if (ret <> LIBC_SUCCESS) then
    ErrorMessage('pthread_mutex_lock');
end;

function TMutex.TryLock: boolean;
var
  ret : integer;

begin
  ret := pthread_mutex_trylock(FMtx);
  Result := (ret = LIBC_SUCCESS);
end;

 

Locking the Mutex – Windows Version

Under Windows, the Lock method waits for the object to be signaled, while the TryLock does not attempt to wait.

procedure TMutex.Lock;
var
  ret : DWORD;

begin
  ret := WaitForSingleObject(FMtx,INFINITE);
  if (ret <> WAIT_OBJECT_0) then
    ErrorMessage('WaitForSingleObject');
end;

function TMutex.TryLock: boolean;
var
  ret : DWORD;

begin
  ret := WaitForSingleObject(Fmtx,0);
  Result := (ret = WAIT_OBJECT_0);
end;

Unlocking the Mutex – Linux Version

Unlocking a mutex calls the unlock API.

procedure TMutex.UnLock;
var
  ret : integer;

begin  
  ret := pthread_mutex_unlock(FMtx);
  if (ret <> LIBC_SUCCESS) then
    ErrorMessage('pthread_mutex_unlock');
end;

 

Unlocking the Mutex – Windows Version

In Windows, unlocking releases the mutex.

procedure TMutex.UnLock;
begin
  if not ReleaseMutex(FMtx) then
    ErrorMessage('ReleaseMutex');
end;

Typical Usage of a Mutex

Shown below is a typical usage of how the TMutex class would be used to protect a resource.

// lock the resource
mtx.Lock;
try  
  // critical block of code that needs to be protected
  global_var := global_var + complex_func(some_parms);
finally  
  mtx.Unlock;
end;

 

Creating a Condition Variable Class

As we learned in a previous section, Condition Variables wait for a defined condition to occur. They need to be able to wait for a condition as well as signal one or more waiting threads. For simplicity sake, the code to handle the process shared flag has been omitted.

Class Definition

TConditionVar = class(TIPCBase)
private
  FCnd      : TCondVar;
  FMutex    : TMutex;
  bOwnMutex : Boolean;
public
  constructor Create(Mutex : TMutex);
  destructor  Destroy; override;
  procedure Wait;
  procedure Signal;     // signal only one waiting thread
  procedure Broadcast;  // signal all of the waiting threads
  function TimedWait(absTime : TTimeSpec) : boolean;
  property Mutex : TMutex read FMutex;
end;

 

Constructor

Multiple condition variables can share the same mutex. In order to accommodate this requirement, the constructor requires a TMutex object or nil. When nil is specified, the constructor creates it’s own mutex otherwise it will use the mutex that was supplied. After handling the mutex object, it then initializes the condition variable. Any errors raise exceptions.

constructor TConditionVar.Create(Mutex : TMutex);
var
  ret : integer;

begin   
  inherited Create;
  // create a mutex to be associated with this condition var
  if not Assigned(Mutex) then
  begin
    bOwnMutex := true;
    FMutex := TMutex.Create(maDefault);
  end
  else
  begin
    bOwnMutex := false;
    FMutex    := Mutex;
  end;

  // initialize the condition variable
  ret := pthread_cond_init(FCnd,nil);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('pthread_cond_init');
end;

 

Destructor

The destructor cleans up by releasing the mutex and the condition variable.

destructor TConditionVar.Destroy;
var
  ret : integer;

begin  
  // first thing, free up the mutex associated with this cond var.
  // but only if we created it
  if bOwnMutex then
    FMutex.Free;

  // free the condition variable  
  ret := pthread_cond_destroy(FCnd);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('pthread_cond_init');

  inherited;
end;

 

Waiting

When waiting on a condition variable, the mutex must already be locked. Both pthread_cond_wait and pthread_cond_timedwait first release the mutex, sleep until the condition occurs then lock the mutex again once they have been signaled. The TimedWait method takes and absolute time, that is, a specific time in the future to return if it has not been signaled.

procedure TConditionVar.Wait;
var
  ret : integer;

begin  ret := pthread_cond_wait(FCnd,FMutex.FMtx);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('pthread_cond_wait');
end;

function TConditionVar.TimedWait(absTime: TTimeSpec): boolean;
var
  ret : integer;

begin
  ret := pthread_cond_timedwait(FCnd,FMutex.FMtx,absTime);
  Result := (ret = LIBC_SUCCESS);
end;

 

Signaling and Broadcasting

When a condition is ready to be signaled, it can alert one or more threads. The Signal method releases only one thread, and the Broadcast method releases all waiting threads.

procedure TConditionVar.Signal;
var
  ret : integer;

begin  ret := pthread_cond_signal(FCnd);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('pthread_cond_signal');
end;

procedure TConditionVar.Broadcast;
var
  ret : integer;

begin  ret := pthread_cond_broadcast(FCnd);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('pthread_cond_broadcast');
end;

Typical Usage – Signal/Broadcasting

Below, the code segment shows a typical usage of signaling or broadcasting any waiting threads. Notice that the mutex must be locked before the waiting thread(s) are signaled.

// first lock the mutex associated with the condition var
condvar.Mutex.Lock
try
  // set the condition true
  condition := true;
  // now signal or broadcast this
  condvar.Signal;
finally
  // and now we can release the mutex
  condvar.Mutex.Unlock;
end;

 

Typical Usage – Waiting

Waiting for a condition requires some additional code. The associated mutex must be first locked. Then as long as the programmer-defined condition has not been met, the condition variable waits. Internally, the wait releases the associated mutex, and sleeps, waiting to be notified. Once signaled, wait locks the associated mutex again. The condition needs to be re-verified again to make sure that another thread has not already changed the condition. Once the condition is true, the loop terminates and the condition is set to false. Finally, the associated mutex is released.

// first lock the mutex associated with the condition var
condvar.Mutex.Lock;
try
  // while the condition hasn't been triggered
  while (condition = false) do
    condvar.Wait;  // wait for the condition to occur
  // now modify the condition
  condition := false;
finally
  // now we can release the mutex
  condvar.Mutex.Unlock;
end;  

 

Creating a POSIX Semaphore Class

In Linux, specifically glibc version 2.2, POSIX Semaphores are only partially implemented. The sem_open function will fail with an error that indicates the function is has not been implemented (error number 38). Also, support for process-shared semaphores has not been implemented either. So the implementation shown below will only work with threads.

Class Definition

TPosixSemaphore = class (TIPCBase)
private
  FSem    : TSemaphore;
  function GetCurrentValue: integer;
public
  Constructor Create(initialValue : integer = 0);
  Destructor Destroy; override;
  procedure Wait;
  function  TryWait : boolean;
  procedure Post;
published
  property CurrentValue : integer read GetCurrentValue;
end;

Constructor

Initialization of the semaphore occurs in the constructor, setting the intial value. Notice that the default is zero. An error will raise an exception.

constructor TPosixSemaphore.Create(initialValue : integer = 0);
var
  ret : integer;

begin
  inherited Create;

  // NOTE: Currently LINUX does not support process-shared semaphores
  ret := sem_init(FSem, false, initialValue);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('sem_init');
end;

Destructor

The semaphore is destroyed and any errors will raise an exception.

destructor TPosixSemaphore.Destroy;
var
  ret : integer;

begin
  ret := sem_destroy( FSem );
  if ret <> LIBC_SUCCESS then
    ErrorMessage('sem_destroy');
  inherited;
end;

Waiting

There are two ways of waiting. For a blocking wait, use the Wait method, while the TryWait will not block. Any errors will raise an exception. Remember when waiting for a semaphore, the semaphore is waiting for the count to be greater than zero, and will decrement the value once it is notified.

procedure TPosixSemaphore.Wait;
var
  ret : integer;

begin
  ret := sem_wait(FSem);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('sem_wait');
end;

function TPosixSemaphore.TryWait: boolean;
var
  ret : integer;

begin
  ret := sem_trywait(FSem);
  Result := (ret = LIBC_SUCCESS);
end;

Posting

Posting increments the count of the semaphore and if any thread is blocked, it will awaken a thread.

procedure TPosixSemaphore.Post;
var
  ret : integer;

begin
  ret := sem_post(FSem);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('sem_post');
end;

 

Current Value

To examine the current value of the semaphore, use the CurrentValue property of the TPosixSemaphore class. This will return the value at the time of the call and is subject to change without notice. In other words, do not use the CurrentValue for anything other than reporting the value at the moment in time of the call.

function TPosixSemaphore.GetCurrentValue: integer;
var
  ret : integer;

begin
  ret := sem_getvalue( FSem,Result);
  if ret <> LIBC_SUCCESS then
    ErrorMessage('sem_getvalue');
end;

 

Using the Classes

Mutex Example: A File Logging Class

One of the best ways to debug multi-threaded code is by logging debug statements as the program runs. Sometimes, in the attempt to track down those nasty multi-threaded bugs, a debugger gets in the way, or even worse, changes the behavior. These bugs can seemingly disappear and can be frustrating. In order to prevent multiple threads from writing at the same time, the log method is protected with a mutex.

unit DebugLogger;

interface

uses
  Mutex;

const
  // these are arbitrary levels that can be changed
  LOG_ALL      = 0; // show even if level is zero
  LOG_MINOR    = 1; // show minor stuff
  LOG_FUNCS    = 2; // show function all function level
  LOG_MAJOR    = 3; // show major stuff
  LOG_PARANOID = 4; // show everything

type
  TDebugLogger = class
  private
    FFileName   : string;
    FOverwrite  : boolean;
    FMtx        : TMutex;
    FDebugLevel : integer;
    function GetThreadID : integer;
  public
    constructor Create(const FileName : string;
                       bOverwrite : boolean;
                       InitialDebugLevel : integer);
    destructor Destroy; override;
    procedure Log(LogLevel : integer; const Msg : string); overload;
    procedure Log(LogLevel : integer; const Msg: string; 
                  const Args: array of const); overload;
  published
    property DebugLevel : integer read FDebugLevel write FDebugLevel;
  end;

implementation

{$IFDEF LINUX}
uses
  SysUtils,Libc;
{$ENDIF}

{$IFDEF WIN32}
uses
  SysUtils,Windows;
{$ENDIF}

{ TDebugLogger }

constructor TDebugLogger.Create(const FileName: string;
  bOverwrite: boolean; InitialDebugLevel : integer);
begin
  inherited Create;
  FFileName   := FileName;
  FOverwrite  := bOverwrite;
  FDebugLevel := InitialDebugLevel;
  // create the mutex
  FMtx := TMutex.Create(maDefault);
end;

destructor TDebugLogger.Destroy;
begin
  // get rid of the mutex
  FMtx.Free;
  inherited;
end;

procedure TDebugLogger.Log(LogLevel : integer; const msg: string);
const
  DATEFORMAT = 'yyyy/mm/dd hh:nn:ss.zzz';

var
  f : TextFile;

begin
  // no use locking the mutex if we're not going to write this out..
  if LogLevel > FDebugLevel then
    Exit;

  // first lock the mutex
  FMtx.Lock;
  try
    AssignFile(F, FFileName);
    if (FileExists(FFileName)) and (not FOverwrite) then
      Append(F)
    else
      Rewrite(F);
    // we only want to overwrite the file the first time,
    // not on subsequent calls.
    FOverwrite := false;
    try
      writeln(F,FormatDateTime(DATEFORMAT,now),' ',GetThreadID,' ',msg);
    finally
      CloseFile(F);
    end;
  finally
    // unlock the mutex
    FMtx.UnLock;
  end;
end;

function TDebugLogger.GetThreadID: integer;
begin
{$IFDEF WIN32}
  Result := GetCurrentThreadID;
{$ENDIF}

{$IFDEF LINUX}
  Result := pthread_self;
{$ENDIF}
end;

procedure TDebugLogger.Log(LogLevel: integer; const Msg: string;
  const Args: array of const);
begin
  Log(LogLevel,Format(Msg,Args));
end;

end.

Condition Variable Example: A Thread-Safe Queue

When developing threaded programs, it is common to have one or more threads (the producers) generate data that other thread(s) (consumers) needs to use. A thread-safe queue class can make development of such a program much easier. In this example, notice that there are two condition variables: Empty and Full. When a consumer is removing an object from the queue and it is empty, it will wait until an object is placed on the queue or until the queue is destroyed. Similarly, when a producer is adding an object to the queue and it is full, it will wait until an object is removed or until the queue is being destroyed.

unit Queue;

interface

uses
  Classes, IPCBase, Mutex;

type
  TQueueType = (qtLifo, qtFifo);

  TThreadSafeQueue = class(TIPCBase)
  protected
    FCondFull  : TConditionVar;
    FCondEmpty : TConditionVar;
    FList      : TStrings;
    FQueueType : TQueueType;
    FMaxSize   : integer;
    FDone      : boolean;
  public
    constructor Create(QueueType : TQueueType; MaxSize : integer);
    destructor  Destroy; override;
    procedure   AddObject(obj : TObject);
    function    GetObject : TObject;
  end;

implementation

uses
  SysUtils;

{ TThreadSafeQueue }

procedure TThreadSafeQueue.AddObject(obj: TObject);
var
  bEmpty : boolean;

begin
  FCondFull.Mutex.Lock;
  try
    // If we're full, we need to wait
    while (FList.Count = FMaxsize) and (not FDone) do
      FCondFull.Wait;

    // The FDone variable indicates that Free has been
    // called. we don't want to do anything at this point
    // other than exit this method.
    if not FDone then
    begin
      // see if we will need to signal the Empty Condtion later
      bEmpty := FList.Count = 0;

      // add the object to the list
      case FQueueType of
        qtFifo : Flist.InsertObject(0,'',obj);
        qtLifo : FList.AddObject('',obj);
      else
        raise Exception.Create('AddObject: Unknown QueueType');
      end;

      // now signal the Empty condition, if necessary
      if bEmpty then
        FCondEmpty.Signal;
    end;

  finally
    FCondFull.Mutex.Unlock;
  end;
end;

constructor TThreadSafeQueue.Create(QueueType: TQueueType;
  MaxSize: integer);
begin
  inherited Create;
  FDone          := false;
  FQueueType     := QueueType;
  FMaxSize       := MaxSize;

  FCondFull      := TConditionVar.Create(nil);
  FCondEmpty     := TConditionVar.Create(FCondFull.Mutex);

  FList          := TStringList.Create;

  Flist.Capacity := FMaxSize;
end;

destructor TThreadSafeQueue.Destroy;
begin
  FDone := true;
  // now notify the condition variables
  FCondFull.Signal;
  FCondEmpty.Signal;
  FCondFull.Free;
  FCondEmpty.Free;
  FList.Free;
  inherited;
end;

function TThreadSafeQueue.GetObject: TObject;
var
  bFull : boolean;
  slot  : integer;

begin
  Result := nil;
  FCondFull.Mutex.Lock;
  try
    // see if we have an empty queue
    while (FList.Count = 0) and (not FDone) do
      FCondEmpty.Wait;

    // The FDone variable indicates that Free has been
    // called. we don't want to do anything at this point
    // other than exit this method.
    if not FDone then
    begin
      // see if we need to notify the full later
      bFull := FList.Count = FMaxSize;

      case FQueueType of
        qtFifo : slot := 0;
        qtLifo : slot := FList.Count-1;
      else
        raise Exception.Create('GetObject: Unknown QueueType');
      end;

      // get the object and remove it from the list
      Result := FList.Objects[slot];
      FList.Delete(slot);

      // now, if the queue was previously full, we need to
      // notify the condition
      if bFull then
        FCondFull.Signal;
    end;

  finally
    FCondFull.Mutex.UnLock;
  end;
end;

end.

 

POSIX Semaphore Example: A Double Buffered File Copier

Double buffering is frequently used to speed up the copying of data. One thread reads data into a buffer, notifies the writer thread, and then continues to read more data as long as there is room in the buffer. The writer thread waits until there is data, writes it to the file, then notifies the reader thread that it is done with the buffer. The major advantage to a double buffering solution is that writes occur as fast as they can. This is important when burning cd-roms and when writing to tapes.

unit FileReaderAndWriterThreads;

interface

uses
  Classes,Semaphore;

const
  NUMBUFFERS = 2;
  BUFSIZE = 1024;

type
  TRWBuffer = record
    data : array[0..BUFSIZE-1] of char;
    size : integer;
  end;

  TReaderThread = class(TThread)
  private
    FFname    : string;
    FWriteSem : TPosixSemaphore;
    FReadSem  : TPosixSemaphore;
  protected
    procedure Execute; override;
    procedure ValidateProperties;
  public
    property Filename       : string          read  FFname    
                                              write FFname;
    property ReadSemaphore  : TPosixSemaphore read  FReadSem  
                                              write FReadSem;
    property WriteSemaphore : TPosixSemaphore read  FWriteSem 
                                              write FWriteSem;
  end;

  TWriterThread = class(TThread)
  private
    FFname    : string;
    FWriteSem : TPosixSemaphore;
    FReadSem  : TPosixSemaphore;
  protected
    procedure Execute; override;
    procedure ValidateProperties;
  public
    property Filename       : string          read  FFname    
                                              write FFname;
    property ReadSemaphore  : TPosixSemaphore read  FReadSem  
                                              write FReadSem;
    property WriteSemaphore : TPosixSemaphore read  FWriteSem 
                                              write FWriteSem;
  end;


implementation

uses
  SysUtils;

var
  theBuffer : array[0..NUMBUFFERS-1] of TRWBuffer;

{ TReaderThread }

procedure TReaderThread.Execute;
var
  i   : integer;
  InF : File;

begin
  try
    ValidateProperties;
    AssignFile(InF,FFname);
    Reset(InF,1);
    try
      i := 0;
      while true do
      begin
        // Wait for a read buffer to be available
        FReadSem.Wait;

        // read the next block of data
        BlockRead(InF,theBuffer[i].data,sizeof(theBuffer[i].data),
                  theBuffer[i].size);

        if theBuffer[i].size = 0 then
        begin
          // we hit the end of the file
          FWriteSem.Post;
          break;
        end;

        inc(i);
        // treat this buffer like a circular one..
        if (i >= NUMBUFFERS) then
          i := 0;
 
        FWriteSem.Post;
      end;
    finally
      CloseFile(InF);
    end;
  except 
    on e : exception do
    begin
      writeln('ReaderThread: Exception fired! ',e.message);
      Exit;      
    end;
  end;
end;

procedure TReaderThread.ValidateProperties;
begin
  if FFname = '' then
    raise Exception.Create('the FileName property must be specified');

  if not FileExists(FFname) then
    raise Exception.CreateFmt('File %s does not exist!',[FFname]);

  if not Assigned(FReadSem) then
    raise Exception.Create('ReadSemaphore property must be specified!');

  if not Assigned(FWriteSem) then
    raise Exception.Create('WriteSemaphore property must be specified');
end;

{ TWriterThread }

procedure TWriterThread.Execute;
var
  i,recs : integer;
  OutF : File;

begin
  try
    ValidateProperties;
    AssignFile(OutF, FFname);
    Rewrite(OutF,1);
    try
      i := 0;
      while true do
      begin
        // wait for a buffer to write
        FWriteSem.Wait;

        // see if we hit the end of the file
        if theBuffer[i].size = 0 then
          break;

        BlockWrite(OutF, theBuffer[i].data, theBuffer[i].size, recs);

        inc(i);
        if i >= NUMBUFFERS then
          i := 0;

        // notify the readers there is slot available now
        FReadSem.Post;
      end;
    finally
      CloseFile(OutF);
    end;
  except 
    on e : exception do
    begin
      writeln('WriterThread: Exception fired! ',e.message);
      Exit;       
    end;
  end;
end;

procedure TWriterThread.ValidateProperties;
begin
  if FFname = '' then
    raise Exception.Create('the FileName property must be specified');

  if FileExists(FFname) then
    raise Exception.CreateFmt('File %s exists!',[FFname]);

  if not Assigned(FReadSem) then
    raise Exception.Create('ReadSemaphore property must be specified!');

  if not Assigned(FWriteSem) then
    raise Exception.Create('WriteSemaphore property must be specified');
end;

end.

// ThreadedCopy.dpr

program ThreadedCopy;

{$APPTYPE CONSOLE}

uses
  FileReaderAndWriterThreads in 'FileReaderAndWriterThreads.pas',
  Semaphore in 'Semaphore.pas';

var
  readthrd  : TReaderThread;
  writethrd : TWriterThread;
  readsem   : TPosixSemaphore;
  writesem  : TPosixSemaphore;

begin
  if ParamCount < 2 then
  begin
    writeln('ThreadedCopy: file1 file2');
    Exit;
  end;

  // Create our semaphores..
  // the read semaphore starts out with NUMBUFFERS slots available
  readsem := TPosixSemaphore.Create(NUMBUFFERS);
  // the write semaphore starts out with zero buffers available.
  writesem := TPosixSemaphore.Create(0);

  readthrd := TReaderThread.Create(true);
  readthrd.Filename := ParamStr(1);
  readthrd.ReadSemaphore := readsem;
  readthrd.WriteSemaphore := writesem;

  writethrd := TWriterThread.Create(true);
  writethrd.Filename := ParamStr(2);
  writethrd.ReadSemaphore := readsem;
  writethrd.WriteSemaphore := writesem;

  // start the threads..
  readthrd.Resume;
  writethrd.Resume;

  // wait for them
  readthrd.WaitFor;
  writethrd.WaitFor;

  writeln('Done with ThreadedCopy');
end.

 

Additional Resources

Kylix 2 Development by Eric Whipple and Rick Ross, published by Wordware, ISBN # 1556227744

Understanding POSIX Threads: Programming with POSIX Threads by David R. Butenhof, published by Addison-Wesley, ISBN # 0-201-63392-2.

Interprocess Communication: UNIX Network Programming Interprocess Communications Volume 2, Second Edition by W. Richard Stevens, published by Prentice Hall, ISBN 0-13-081081-9

LINUX Core Kernel Commentary by Scott Maxwell, published by Coriolis, ISBN 1-57610-469-9

Interesting notes on the Linux Kernel and Threads: http://boudicca.tux.org/hypermail/linux-kernel/1998/1998week50/0701.html

POSIX and Unix Threads:: http://www-users.cs.umn.edu/~seetala/ResourceCenter/systems-programming.html

POSIX Threads Explained: http://www-106.ibm.com/developerworks/library/posix1.html?dwzone=linux

Additional online resources can be found at http://rick-ross.com

Source Code

Paper Specific

Leave a Reply