Rick Ross – PILLAR 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 |
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