• No results found

Waiting on multiple ‘handle’ objects (Windows only)

On Windows, all of TEvent, TMutex and TSemaphore have ‘handles’, allowing a thread to wait on more than one of them at the same time. At the Windows API level, this is done by calling either WaitForMultipleObjects, WaitForMultipleObjectsEx or the COM equivalent, CoWaitForMultipleHandles, as appropriate. While any of these routines could be called directly, the

THandleObject class provides a handy WaitForMultiple class method that wraps them nicely:

type

THandleObjectArray = array of THandleObject;

TWaitResult = (wrSignaled, wrTimeout, wrAbandoned, wrError, wrIOCompletion);

class function WaitForMultiple(const Objs: THandleObjectArray;

Timeout: LongWord; All: Boolean; out Signaled: THandleObject;

UseCOMWait: Boolean = False; Len: Integer = 0): TWaitResult;

The Timeout parameter specifies how many milliseconds the function will wait for the objects to signal; as with the regular

WaitFor methods, pass INFINITE if you do not want to ever time out. When All is True, the function only returns when every object listed has signalled or aborted, otherwise the function returns when at least one of the objects has. On return,

Signaled will contain the first object that signalled. With respect to the optional parameters, UseCOMWait determines whether CoWaitForMultipleHandles rather than WaitForMultipleObjectsEx is used under the hood, and when set, Len

determines the number of objects in the source array to wait for (if Len is left as 0, all of them will be).

A common (though by no means only) use of WaitForMultiple is in situations where a thread wishes to wait on both the thing it is interested in and a separately-managed cancellation event. This was the case in the interprocess semaphore demo presented earlier:

var

Objs: THandleObjectArray;

Semaphore: TSemaphore;

SignalledObj: THandleObject;

begin //...

Objs := THandleObjectArray.Create(Semaphore, FTerminateEvent);

if (THandleObject.WaitForMultiple(Objs, INFINITE, False, SignalledObj) <> wrSignaled) or

(SignalledObj <> Semaphore) then Exit;

With AAll set to False, WaitForMultiple returns as soon as one of the objects waited on signals; if that was the termination event, then the thread gracefully exits.

Condition variables (TConditionVariableMutex, TConditionVariableCS)

Being based on Unix, OS X implements the POSIX threading API (‘pthreads’). In itself, this API supports neither Windows-style threading events nor the ability to wait on more than one signalling object at the same time. Instead, it has

‘condition variables’. These work in harness with a locking object of some sort: in use, a thread first acquires the lock, before waiting on the condition variable to signal or ‘pulse’. Internally, going into the waiting state involves the lock being released, allowing other threads — potentially other waiters, but ultimately the thread that will be doing the pulsing — to take the lock themselves. Once pulsed, a thread is woken and given back the lock.

The role of the condition variable in all this is to enforce atomicity, both with respect to releasing the lock to go into a waiting state, and being woken with the lock reacquired. The condition variable also ensures waiting threads are woken in an orderly fashion, if more than one is pulsed at the same time.

As POSIX doesn’t support Windows-style events, so the Windows API did not originally support condition variables.

Latterly Microsoft has added a condition variable primitive however. Nonetheless, there is still a slight difference, since where the locking object used with a pthreads condition variable is a mutex, the one used with the Windows API is a critical section. Consequently, System.SyncObjs provides two condition variable classes, TConditionVariableMutex for working with a TMutex and TConditionVariableCS for working with a TCriticalSection or TRTLCriticalSection. By backfilling implementations as necessary, these two classes work on all supported platforms, including older versions of Windows that don’t natively support condition variables at all.

Here’s a simple example of TConditionVariableMutex in use:

uses

System.SysUtils, System.Classes, System.SyncObjs;

var

Lock: TMutex;

CondVar: TConditionVariableMutex;

I: Integer;

begin

Lock := TMutex.Create;

CondVar := TConditionVariableMutex.Create;

try

for I := 1 to 2 do

TThread.CreateAnonymousThread(

procedure

WriteLn('Condition variable will be signalled in 1s...');

Sleep(1000);

CondVar.ReleaseAll; //signal to all waiters ReadLn; condition variable — in practice, the first thread calling WaitFor will allow the second to get past the Acquire stage and call

WaitFor itself. After dawdling for a short period of time, the main thread signals to all waiting threads, causing the two in TConditionVariableMutex/TConditionVariableCS terms, this means calling either Release or ReleaseAll. Furthermore, if no thread is waiting at the time the condition variable is signalled, then the pulse is ‘lost’:

uses

System.SysUtils, System.Classes, System.SyncObjs;

var

Lock: TCriticalSection;

CondVar: TConditionVariableCS;

begin

Lock := TCriticalSection.Create;

CondVar := TConditionVariableCS.Create;

try

CondVar.Release;

TThread.CreateAnonymousThread(

procedure

In this example, the background thread’s WaitFor call times out since Release was called by the main thread before WaitFor

was called. This behaviour contrasts with events, with which a signal from a SetEvent call is never lost so long as the event object itself has not been destroyed:

var

Event: TEvent; //or TLightweightEvent begin

Event := TEvent.Create;

try

Event.SetEvent;

TThread.CreateAnonymousThread(

procedure

A ‘monitor’ in a multithreading context is an object that combines a lock and condition variable. In Delphi, TMonitor is a record type defined in the System unit. We have already seen it in its role as a critical section mechanism that works against any class instance. For the condition variable part, it adds Wait, Pulse and PulseAll methods, which are equivalent to the WaitFor, Release and ReleaseAll methods of TConditionVariableMutex and TConditionVariableCS.

In practice, the wait/pulse functionality of TMonitor is not something you would use much (if at all) in application-level code. However, it can form the nuts and bolts of higher level primitives, and indeed, the TCountdownEvent,

TLightweightEvent and TLightwightSemaphore classes we met earlier are all based around it. Beyond that, it can play a crucial part in solving ‘producer/consumer’ problems.

TThreadedData<T> = class strict private

FData: T;

FEmpty: Boolean;

FLock: TObject;

public

constructor Create;

destructor Destroy; override;

function Read: T;

procedure Write(const AData: T);

end;

Here, the constructor will just initialise FEmpty to True and create the dummy object to lock against; the destructor will then just free the dummy object:

constructor TThreadedData<T>.Create;

begin

inherited Create;

FEmpty := True;

FLock := TObject.Create;

end;

destructor TThreadedData<T>.Destroy;

begin

FLock.Free;

inherited Destroy;

end;

In the case of Read, we will wait on FEmpty becoming False, before changing it to True and issuing the signal; in the case of

Write, we wait on FEmpty becoming True, before changing it to False and issuing the signal:

function TThreadedData<T>.Read: T;

begin

TMonitor.Enter(FLock);

try

while FEmpty do

TMonitor.Wait(FLock, INFINITE);

Result := FData;

FEmpty := True;

TMonitor.PulseAll(FLock);

finally

TMonitor.Exit(FLock);

end;

end;

procedure TThreadedData<T>.Write(const AData: T);

begin

TMonitor.Enter(FLock);

try

while not FEmpty do

TMonitor.Wait(FLock, INFINITE);

FEmpty := False;

FData := AData;

TMonitor.PulseAll(FLock);

finally

TMonitor.Exit(FLock);

end;

end;

The Wait call in both cases should be made in the context of a loop to cover the case of more than one waiting thread.

Putting this class to use, in the following example the main thread acts as the producer, feeding the consumer a list of strings:

uses

System.SysUtils, System.StrUtils, System.Classes, System.SyncObjs;

procedure WorkerThreadProc(const AData: TThreadedData<string>);

var

S: string;

begin

{ Enter a loop in which we wait for work, process the work given, wait for work, process the work given, and so on...}

repeat

S := AData.Read;

if S = '' then Exit;

Sleep(500); //simulate doing some 'proper' work...

Write(ReverseString(S) + ' ');

until False;

end;

var

Data: TThreadedData<string>;

FinishedSemaphore: TSemaphore;

S: string;

begin

Data := TThreadedData<string>.Create;

FinishedSemaphore := TSemaphore.Create(nil, 0, 1, '');

try

//spin up the worker thread TThread.CreateAnonymousThread(

procedure begin

WorkerThreadProc(Data);

FinishedSemaphore.Release;

end).Start;

//write the strings to process for S in TArray<string>.Create(

'siht', 'si', 'a', 'omed', 'fo', 'rotinoMT') do Data.Write(S);

//an empty string denotes the end of the process Data.Write('');

//wait for the worker to finish FinishedSemaphore.WaitFor;

WriteLn(SLineBreak + 'Finished!');

finally

FinishedSemaphore.Free;

Data.Free;

end;

end.

The output is This is a demo of TMonitor, each word coming up in turn after a short delay.