1. Introduction to OmniThreadLibrary

OmniThreadLibrary is a multithreading library for Delphi, written mostly by the author of this book (see Credits for full list of contributors). OmniThreadLibrary can be roughly divided into three parts. Firstly, there are building blocks that can be used either with the OmniThreadLibrary threading helpers or with any other threading approach (f.i. with Delphi’s TThread or with AsyncCalls). Most of these building blocks are described in chapter Miscellaneous, while some parts are covered elsewhere in the book (Lock-free Collections, Blocking collection, Synchronization).

Secondly, OmniThreadLibrary brings low-level multithreading framework, which can be thought of as a scaffolding that wraps the TThread class. This framework simplifies passing messages to and from the background threads, starting background tasks, using thread pools and more.

Thirdly, OmniThreadLibrary introduces high-level multithreading concept. High-level framework contains multiple pre-packaged solutions (so-called abstractions; f.i. parallel for, pipeline, fork/join …) which can be used in your code. The idea is that the user should just choose appropriate abstraction and write the worker code, while the OmniThreadLibrary provides the framework that implements the tricky multithreaded parts, takes care of synchronisation and so on.

1.1 Requirements

OmniThreadLibrary requires at least Delphi 2007 and doesn’t work with FreePascal. The reason for this is that most parts of OmniThreadLibrary use language constructs that are not yet supported by the FreePascal compiler.

High-level multithreading framework requires at least Delphi 2009.

OmniThreadLibrary currently only targets Windows installation. Both 32-bit and 64-bit platform are supported.

1.2 License

OmniThreadLibrary is an open-sourced library with the OpenBSD license.

This software is distributed under the BSD license.

Copyright (c) 2017, Primoz Gabrijelcic

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

In short, this means that:

  1. You can use the library in any project, free, open source or commercial, without having to mention my name or the name of the library anywhere in your project, documentation or on the web site.
  2. You can change the source for your own use. You can also put a modified version on the web, but you must not remove my name or the license from the source code.
  3. I’m not guilty if the software blows in your face. Remember, you got OmniThreadLibrary for free.

In case your company would like to get support contract for the OmniThreadLibrary, please contact me.

1.3 Installation

  1. Download the last stable edition (download link is available at the OmniThreadLibrary site, or download the latest state from the repository. Typically, it is safe to follow the repository trunk as only tested code is committed. [Saying that, I have to admit that from time to time a bug or two do creep in but they are promptly exterminated].
  2. If you have downloaded the last stable edition, unpack it to a folder.
  3. Add the folder where you [unpacked last stable edition/checked out the SVN trunk] to the Delphi’s Library path. Also add the src subfolder to the Library path. In case you are already using units from my GpDelphiUnits project, you can ignore the copies in the src folder and use GpDelphiUnits version.
  4. Add necessary units to the uses statement and start using the library!

1.3.1 Installing Design Package

OmniThreadLibrary includes one design-time component (TOmniEventMonitor) which may be used to receive messages sent from the background tasks and to monitor thread creation/destruction. It is used in some of the demo applications.

To compile and install the package containing this component, follow these steps:

You should repeat these steps whenever the OmniThreadLibrary installation is updated.

1.4 Why Use OmniThreadLibrary?

OmniThreadLibrary approaches the threading problem from a different perspective than TThread. While the Delphi’s native approach is oriented towards creating and managing threads on a very low level, the main design guideline behind OmniThreadLibrary is: “Enable the programmer to work with threads in as fluent way as possible.” The code should ideally relieve you from all burdens commonly associated with multithreading.

OmniThreadLibrary was designed to become a “VCL for multithreading” – a library that will make typical multithreading tasks really simple but still allow you to dig deeper and mess with the multithreading code at the operating system level. While still allowing this low-level tinkering, OmniThreadLibrary allows you to work on a higher level of abstraction most of the time.

There are two important points of distinction between TThread and OmniThreadLibrary, both explained further in this chapter. One is that OmniThreadLibrary focuses on tasks, not threads and another is that in OmniThreadLibrary messaging tries to replace locking whenever possible.

By moving most of the critical multithreaded code into reusable components (classes and high-level abstractions), OmniThreadLibrary allows you to write better multithreaded code faster.

1.5 Tasks vs. Threads

In OmniThreadLibrary you don’t create threads but tasks. A task can be executed in a new thread or in an existing thread, taken from the thread pool.

A task is created using CreateTask function, which takes as a parameter a global procedure, a method, an instance of a TOmniWorker class (or, usually, a descendant of that class) or an anonymous method (in Delphi 2009 and newer). CreateTask returns an IOmniTaskControl interface, which can be used to control the task. A task is always created in suspended state and you have to call Run to activate it (or Schedule to run it in a thread pool).

The task has access to the IOmniTask interface and can use it to communicate with the owner (the part of the program that started the task). Both interfaces are explained in full detail in chapter Low-level multithreading.

The distinction between the task and the thread can be summarized in few simple words.

Task is part of code that has to be executed.

Thread is the execution environment.

You take care of the task, OmniThreadLibrary takes care of the thread.

1.6 Locking vs. Messaging

I believe that locking is evil. It leads to slow code and deadlocks and is one of the main reasons for almost-working multithreaded code (especially when you use shared data and forget to lock it up). Because of that, OmniThreadLibrary tries to move as much away from the shared data approach as possible. Cooperation between threads is rather achieved with messaging.

If we compare shared data approach with messaging, both have good and bad sides. On the good side, shared data approach is fast because it doesn’t move data around and is less memory intensive as the data is kept only in one copy. On the bad side, locking must be used to access data which leads to bad scaling (slowdowns when many threads are accessing the data), deadlocks and livelocks.

The situation is almost reversed for the messaging. There’s no shared data so no locking, which makes the program faster, more scalable and less prone to fall in the deadlocking trap. (Livelocking is still possible, though.) On the bad side, it uses more memory, requires copying data around (which may be a problem if shared data is large) and may lead to complicated and hard to understand algorithms.

OmniThreadLibrary uses custom lock-free structures to transfer data between the task and its owner (or directly between two tasks). The system is tuned for high data rates and can transfer more than million messages per second. However, in some situations shared data approach is necessary and that’s why OmniThreadLibrary adds significant support for synchronisation.

Lock-free (or microlocked) structures in OmniThreadLibrary encompass:

OmniThreadLibrary automatically inserts two bounded queues between the task owner (IOmniTaskControl) and the task (IOmniTask) so that the messages can flow in both directions.

1.7 Message Loop Required

Due to implementation details, OmniThreadLibrary requires that each thread owner maintains and processes a message queue. This condition is automatically satisfied in VCL and service applications, but running OmniThreadLibrary threads from a console application requires more work. Additional work is also required if you are creating OmniThreadLibrary threads from background threads.

1.7.1 OmniThreadLibrary and Console

To correctly use OmniThreadLibrary in a console application, said application must process Windows messages. This is demonstrated in the 62_console demo which is reproduced below.

 1 program app_62_console;
 2 
 3 {$APPTYPE CONSOLE}
 4 
 5 uses
 6   Windows, Messages, SysUtils,
 7   OtlComm, OtlTask, OtlTaskControl, OtlParallel;
 8 
 9 const
10   MSG_STATUS = WM_USER;
11 
12   procedure ProcessMessages;
13   var
14     Msg: TMsg;
15   begin
16     while integer(PeekMessage(Msg, 0, 0, 0, PM_REMOVE)) <> 0 do begin
17       TranslateMessage(Msg);
18       DispatchMessage(Msg);
19     end;
20   end;
21 
22   function DoTheCalculation(const task: IOmniTask): integer;
23   var
24     i: integer;
25   begin
26     for i := 1 to 5 do begin
27       task.Comm.Send(MSG_STATUS, '... still calculating');
28       Sleep(1000);
29     end;
30     Result := 42;
31   end;
32 
33 var
34   calc: IOmniFuture<integer>;
35 
36 begin
37   try
38     calc := Parallel.Future<integer>(DoTheCalculation,
39       parallel.TaskConfig.OnMessage(MSG_STATUS,
40         procedure(const task: IOmniTaskControl; const msg: TOmniMessage)
41         begin
42           Writeln(msg.MsgData.AsString);
43         end));
44 
45     Writeln('Background thread is calculating ...');
46     while not calc.IsDone do
47       ProcessMessages;
48     Writeln('And the answer is: ', calc.Value);
49 
50     if DebugHook <> 0 then
51       Readln;
52   except
53     on E: Exception do
54       Writeln(E.ClassName, ': ', E.Message);
55   end;
56 end.

The main program creates a future which runs a function DoTheCalculation and then waits for it to return a value (while not calc.IsDone).

The future waits five seconds and each second sends a message back to the owner (task.Comm.Send(MSG_STATUS, '... still calculating')). Main thread processes these messages in the MSG_STATUS handler.

If you run the program, you’ll see that the message ... still calculating is displayed five times with one second delay between two messages. After that, And the answer is: 42 is displayed.

The critical part of this program are two lines:

1 while not calc.IsDone do
2   ProcessMessages;

If you comment them out, the program will write Background thread is calculating ..., then nothing will happen (visibly) for five seconds and then all messages will be displayed at once.

In this example it is not really critical to process messages as the program would continue to function correctly – more or less – even when message processing is removed, but in other cases all kinds of weird behaviour can occur. OmniThreadLibrary occasionally uses messages for internal purpose and if you prevent processing of these messages, applications may misbehave. The best approach is to always include periodic calls to ProcessMessages in a console application.

1.7.2 OmniThreadLibrary Task Started from another Task

Similar considerations take order when an OmniThreadLibrary task is started from another OmniThreadLibrary task – the ‘intermediate’ thread (the one controlled by the task that creates another task) must process messages. The easiest way to achieve that is by using the MsgWait qualifier when creating a task.

Let’s take a look at an example from the 66_ThreadsInThreads demo.

In this example, a click on a button creates a task that will create a future.

1   FOwnerTask := CreateTask(TWorker.Create(), 'OTL owner')
2     .OnMessage(Self)
3     .OnTerminated(TaskTerminated)
4     .MsgWait // critical, this allows OTL task to process messages
5     .Run;

The critical part here is the call to MsgWait which causes internal loop in the task to process Windows messages. Without this MsgWait the program would stop working.

The worker does all the work in its Initialization method.

 1 function TWorker.Initialize: boolean;
 2 begin
 3   Result := inherited Initialize;
 4   if Result then begin
 5     Task.Comm.Send(WM_LOG, Format('[%d] Starting a Future', [GetCurrentThreadID]));
 6     FCalc := Parallel.Future<integer>(Asy_DoTheCalculation,
 7       Parallel.TaskConfig
 8         .OnMessage(MSG_STATUS,
 9           procedure(const workerTask: IOmniTaskControl; const msg: TOmniMessage)
10           begin
11             // workerTask = task controller for Parallel.Future worker thread
12             // Task = TWorker.Task = interface of the TWorker task
13             Task.Comm.Send(WM_LOG, Format('[%d] Future sent a message: %s',
14               [GetCurrentThreadID, msg.MsgData.AsString]));
15           end)
16         .OnTerminated(
17           procedure
18           begin
19             Task.Comm.Send(WM_LOG, Format('[%d] Future terminated, result = %d',
20               [GetCurrentThreadID, FCalc.Value]));
21             FCalc := nil;
22             Task.Comm.Send(WM_LOG, Format('[%d] Terminating worker', 
23               [GetCurrentThreadID]));
24             // Terminate TWorker
25             Task.Terminate;
26           end));
27   end;
28 end;

This code executes from the context of a background worker thread. It looks complicated, but actually it just creates a Future calculation (FCalc := Parallel.Future<integer>) and sets up event handlers that will process messages sent from the future (.OnMessage) and handle the completion of the future calculation (.OnTerminated).

OnMessage just takes message that was sent from the future, adds some text and current thread ID and forwards message to the form where it is logged in the WMLog method (not shown here).

OnTerminated also logs the event, clears the future interface and terminates self (Task.Terminate). After that, form’s TaskTerminated method (not shown here) is called and cleans the task controller interface.

The future itself does nothing special, it just sends five messages with one second delay between them and then returns a value.

 1 function TWorker.Asy_DoTheCalculation(const task: IOmniTask): integer;
 2 var
 3   i: integer;
 4 begin
 5   for i := 1 to 5 do begin
 6     task.Comm.Send(MSG_STATUS, Format('[%d] ... still calculating', 
 7       [GetCurrentThreadID]));
 8     Sleep(1000);
 9   end;
10   Result := 42;
11 end;

When you run the program and click on the button, following text will be displayed (thread IDs – numbers in brackets – will be different in your case, of course).

We can see that Future messages were generated in thread 18420, then passed through the parent thread 6088 and ended in the main thread 11968.

1.7.3 OmniThreadLibrary Task Started from a TThread

We have to do more work if we want to create a OmniThreadLibrary thread from a basic Delphi TThread thread. We have to make sure that thread messages are periodically process by calling the DSiProcessThreadMessages function from the DSiWin32 unit (or a similar code that calls PeekMessage / TranslateMessage / DispatchMessage).

The 66_ThreadsInThreads demo contains an example of such situation.

We don’t have to do anything special when creating a thread.

1   FThread := TWorkerThread.Create(true);
2   FThread.OnTerminate := ThreadTerminated;
3   FThread.FreeOnTerminate := true;
4   FThread.Start;

The main thread method firstly creates a Future and sets up an .OnMessage handler which just resends messages to the main thread.

 1 procedure TWorkerThread.Execute;
 2 var
 3   awaited: DWORD;
 4   calc   : IOmniFuture<integer>;
 5   handles: array [0..0] of THandle;
 6 begin
 7   Log('Starting a Future');
 8 
 9   calc := Parallel.Future<integer>(Asy_DoTheCalculation,
10     Parallel.TaskConfig
11       .OnMessage(MSG_STATUS,
12         procedure(const workerTask: IOmniTaskControl; const msg: TOmniMessage)
13         begin
14           Log('Future sent a message: ' + msg.MsgData.AsString);
15         end));
16 
17   repeat
18     awaited := MsgWaitForMultipleObjects(0, handles, false, INFINITE, QS_ALLPOSTMESSAGE);
19     if awaited = WAIT_OBJECT_0 + 0 {handle count} then
20       DSiProcessThreadMessages;
21   until calc.IsDone;
22 
23   Log('Future terminated, result = ' + IntToStr(calc.Value));
24   calc := nil;
25   Log('Terminating worker');
26 end;

Then it enters a repeat .. until loop in which it waits for a Windows message (MsgWaitForMultipleObjects), processes all waiting messages (DSiProcessThreadMessages) and checks whether the calculation has completed (calc.IsDone).

At the end it cleans up the future and exits. That destroys the TWorkerThread thread.

Calling MsgWaitForMultipleObjects is not strictly necessary. You could just call DSiProcessThreadMessages from time to time. It does, however, improve the performance as the code uses no CPU in such wait.

If you are already using some other kind of wait-and-dispach mechanism in your thread (WaitForSingleObject, WaitForSingleObjectEx, WaitForMultipleObjects, WaitForMultipleObjectsEx) then they are easy to convert to MsgWaitForMultipleObjects or MsgWaitForMultipleObjectsEx.

Running the program shows similar behaviour as in the previous example.

1.8 TOmniValue

A TOmniValue (part of the OtlCommon unit) is data type which is central to the whole OmniThreadLibrary. It is used in all parts of the code (for example in a communication subsystem) when type of the data that is to be stored/passed around is not known in advance.

It is implemeted as a smart record (a record with functions and operators) which functions similary to a Variant or TValue but is faster12. It can store following data types:

In all cases ownership of reference-counted data types (strings, interfaces) is managed correctly so no memory leaks can occur when such type is stored in a TOmniValue variable.

The TOmniValue type is too large to be shown in one piece so I’ll show various parts of its interface throughout this chapter.

1.8.1 Data Access

The content of a TOmniValue record can be accessed in many ways, the simplest (and in most cases the most useful) being through the AsXXX properties.

 1 property AsAnsiString: AnsiString;
 2 property AsBoolean: boolean;
 3 property AsCardinal: cardinal;
 4 property AsDouble: Double;
 5 property AsDateTime: TDateTime;
 6 property AsException: Exception;
 7 property AsExtended: Extended;
 8 property AsInt64: int64 read;
 9 property AsInteger: integer;
10 property AsInterface: IInterface;
11 property AsObject: TObject;
12 property AsOwnedObject: TObject;
13 property AsPointer: pointer;
14 property AsString: string;
15 property AsVariant: Variant;
16 property AsWideString: WideString;

While the setters for those properties are pretty straightforward, getters all have a special logic built in which tries to convert data from any reasonable source type to the requested type. If that cannot be done, an exception is raised.

For example, getter for the AsString property is called CastToString and in turn calls TryCastToString, which is in turn a public function of TOmniValue.

 1 function TOmniValue.CastToString: string;
 2 begin
 3   if not TryCastToString(Result) then
 4     raise Exception.Create('TOmniValue cannot be converted to string');
 5 end;
 6 
 7 function TOmniValue.TryCastToString(var value: string): boolean;
 8 begin
 9   Result := true;
10   case ovType of
11     ovtNull:       value := '';
12     ovtBoolean:    value := BoolToStr(AsBoolean, true);
13     ovtInteger:    value := IntToStr(ovData);
14     ovtDouble,
15     ovtDateTime,
16     ovtExtended:   value := FloatToStr(AsExtended);
17     ovtAnsiString: value := string((ovIntf as IOmniAnsiStringData).Value);
18     ovtString:     value := (ovIntf as IOmniStringData).Value;
19     ovtWideString: value := (ovIntf as IOmniWideStringData).Value;
20     ovtVariant:    value := string(AsVariant);
21     else Result := false;
22   end;
23 end;

When you don’t know the data type stored in a TOmniValue variable and you don’t want to raise an exception if compatible data is not available, you can use the TryCastToXXX family of functions directly.

 1 function  TryCastToAnsiString(var value: AnsiString): boolean;
 2 function  TryCastToBoolean(var value: boolean): boolean;
 3 function  TryCastToCardinal(var value: cardinal): boolean;
 4 function  TryCastToDouble(var value: Double): boolean;
 5 function  TryCastToDateTime(var value: TDateTime): boolean;
 6 function  TryCastToException(var value: Exception): boolean;
 7 function  TryCastToExtended(var value: Extended): boolean;
 8 function  TryCastToInt64(var value: int64): boolean;
 9 function  TryCastToInteger(var value: integer): boolean;
10 function  TryCastToInterface(var value: IInterface): boolean;
11 function  TryCastToObject(var value: TObject): boolean;
12 function  TryCastToPointer(var value: pointer): boolean;
13 function  TryCastToString(var value: string): boolean;
14 function  TryCastToVariant(var value: Variant): boolean;
15 function  TryCastToWideString(var value: WideString): boolean;

Alternatively, you can use CastToXXXDef functions which return a default value if current value of the TOmniValue cannot be converted into required data type.

 1 function  CastToAnsiStringDef(const defValue: AnsiString): AnsiString;
 2 function  CastToBooleanDef(defValue: boolean): boolean;
 3 function  CastToCardinalDef(defValue: cardinal): cardinal;
 4 function  CastToDoubleDef(defValue: Double): Double;
 5 function  CastToDateTimeDef(defValue: TDateTime): TDateTime;
 6 function  CastToExceptionDef(defValue: Exception): Exception;
 7 function  CastToExtendedDef(defValue: Extended): Extended;
 8 function  CastToInt64Def(defValue: int64): int64;
 9 function  CastToIntegerDef(defValue: integer): integer;
10 function  CastToInterfaceDef(const defValue: IInterface): IInterface;
11 function  CastToObjectDef(defValue: TObject): TObject;
12 function  CastToPointerDef(defValue: pointer): pointer;
13 function  CastToStringDef(const defValue: string): string;
14 function  CastToVariantDef(defValue: Variant): Variant;
15 function  CastToWideStringDef(defValue: WideString): WideString;

They are all implemented in the same value, similar to the CastToObjectDef below.

1 function TOmniValue.CastToObjectDef(defValue: TObject): TObject;
2 begin
3   if not TryCastToObject(Result) then
4     Result := defValue;
5 end;

1.8.2 Type Testing

For situations where you would like to determine the type of data stored inside the TOmniValue, there is the IsXXX family of functions.

 1 function  IsAnsiString: boolean;
 2 function  IsArray: boolean;
 3 function  IsBoolean: boolean;
 4 function  IsEmpty: boolean;
 5 function  IsException: boolean;
 6 function  IsFloating: boolean;
 7 function  IsDateTime: boolean;
 8 function  IsInteger: boolean;
 9 function  IsInterface: boolean;
10 function  IsInterfacedType: boolean;
11 function  IsObject: boolean;
12 function  IsOwnedObject: boolean;
13 function  IsPointer: boolean;
14 function  IsRecord: boolean;
15 function  IsString: boolean;
16 function  IsVariant: boolean;
17 function  IsWideString: boolean;

Alternatively, you can use the DataType property.

1 type
2   TOmniValueDataType = (ovtNull, ovtBoolean, ovtInteger, ovtDouble, ovtObject,
3     ovtPointer, ovtDateTime, ovtException, ovtExtended, ovtString, ovtInterface,
4     ovtVariant, ovtWideString, ovtArray, ovtRecord, ovtAnsiString, ovtOwnedObject);
5 
6 property DataType: TOmniValueDataType;

1.8.3 Clearing the Content

There are two ways to clear a TOmniValue – you can either call its Clear method, or you can assign to it a TOmniValue.Null.

1 procedure Clear;
2 class function Null: TOmniValue; static;

An example:

1 var
2   ov: TOmniValue;
3   
4 ov.Clear;
5 // or
6 ov := TOmniValue.Null;

1.8.4 Operators

TOmniValue also implements several Implicit operators which help with automatic conversion to and from different data types. Internally, they are implemented as assignment to/from the AsXXX property.

 1 class operator Equal(const a: TOmniValue; i: integer): boolean;
 2 class operator Equal(const a: TOmniValue; const s: string): boolean;
 3 class operator Implicit(const a: AnsiString): TOmniValue;
 4 class operator Implicit(const a: boolean): TOmniValue;
 5 class operator Implicit(const a: Double): TOmniValue;
 6 class operator Implicit(const a: Extended): TOmniValue;
 7 class operator Implicit(const a: integer): TOmniValue;
 8 class operator Implicit(const a: int64): TOmniValue;
 9 class operator Implicit(const a: pointer): TOmniValue;
10 class operator Implicit(const a: string): TOmniValue;
11 class operator Implicit(const a: IInterface): TOmniValue; 
12 class operator Implicit(const a: TObject): TOmniValue;
13 class operator Implicit(const a: Exception): TOmniValue;
14 class operator Implicit(const a: TOmniValue): AnsiString;
15 class operator Implicit(const a: TOmniValue): int64;
16 class operator Implicit(const a: TOmniValue): TObject;
17 class operator Implicit(const a: TOmniValue): Double;
18 class operator Implicit(const a: TOmniValue): Exception;
19 class operator Implicit(const a: TOmniValue): Extended;
20 class operator Implicit(const a: TOmniValue): string;
21 class operator Implicit(const a: TOmniValue): integer;
22 class operator Implicit(const a: TOmniValue): pointer;
23 class operator Implicit(const a: TOmniValue): WideString;
24 class operator Implicit(const a: TOmniValue): boolean;
25 class operator Implicit(const a: TOmniValue): IInterface;
26 class operator Implicit(const a: WideString): TOmniValue;
27 class operator Implicit(const a: Variant): TOmniValue;
28 class operator Implicit(const a: TDateTime): TOmniValue;
29 class operator Implicit(const a: TOmniValue): TDateTime;

Implicit conversion to/from TDateTime is supported only in Delphi XE and newer.

Two Equal operators simplify comparing TOmniValue to an integer and string data.

1.8.5 Using with Generic Types

Few methods simplify using TOmniValue with a generic class/record.

1 class function CastFrom<T>(const value: T): TOmniValue; static;
2 function  CastTo<T>: T;
3 function  CastToObject<T: class>: T; overload;
4 function  ToObject<T: class>: T;
5 class function Wrap<T>(const value: T): TOmniValue; static;
6 function  Unwrap<T>: T;

CastFrom<T> converts any type into a TOmniValue. In Delphi 2009, this function is severely limited as only very simple types (integer, object) are supported. In Delphi 2010 and newer, TValue type is used to facilitate the conversion and all data types supported by the TOmniValue can be converted.

CastTo<T> converts TOmniValue into any other type. In Delphi 2009 same limitations apply as for CastFrom<T>.

CastToObject<T> (available in Delphi 2010 and newer) performs a hard cast with no type checking. It is equivalent to using T(AsObject)

ToObject<T> (available in Delphi 2010 and newer) casts the object to type T with type checking. It is equivalent to using AsObject as T.

Wrap<T> [3.06] wraps any data type in an instance of TOmniRecordWrapper<T> and stores this value in a TOmniValue variable.

Unwrap<T> [3.06] unwraps a TOmniValue holding a TOmniRecordWrapper<T> and returns owned value of type T. It has to be called in this form: omnivalue.Unwrap<T>().

Wrap and Unwrap are especially useful as they allow you to store TMethod data (event handlers) in a TOmniValue variable.

1.8.6 Array Access

Each TOmniValue can contain an array of other TOmniValues. Internally, they are stored in a TOmniValueContainer object. This object can be accessed directly by reading the AsArray property.

1 property AsArray: TOmniValueContainer read GetAsArray;

IsArray can be used to test whether a TOmniValue contains an array of values.

Arrays can be accessed by an integer indexes (starting with 0), or by string indexes (named access). Integer-indexed arrays are created by calling TOmniValue.Create and string-indexed arrays are created by calling TOmniValue.CreateNamed.

1 constructor Create(const values: array of const);
2 constructor CreateNamed(const values: array of const);

In the latter case, elements of the values parameter must alternate between names (string indexes) and values.

1 ov := TOmniValue.CreateNamed(
2         ['Key1', 'Value of ov[''Key1'']', 
3          'Key2', 'Value of ov[''Key2'']'
4         ]);

In the example above, both ov[0] and ov['Key1'] would return the same string, namely 'Value of ov[''Key1'']'.

Array elements can be accessed with the AsArrayItem property, by using an integer index (for integer-indexed arrays), a string index (for string-indexed arrays), or a TOmniValue index. In the last case, the type of data stored inside the TOmniValue index parameter will determine how the array element is accessed. This last form is not available in Delphi 2007, where AsArrayItemOV should be used instead.

All forms of AsArrayItem allow extending an array. If you write data into an index which doesn’t already exist, the array will automatically grow to accomodate the new value.

1 property AsArrayItem[idx: integer]: TOmniValue; default;
2 property AsArrayItem[const name: string]: TOmniValue; default;
3 property AsArrayItem[const param: TOmniValue]: TOmniValue; default;
4 property AsArrayItemOV[const param: TOmniValue]: TOmniValue;

If you want to test whether an array element exists, use the HasArrayItem function.

1 function  HasArrayItem(idx: integer): boolean; overload;
2 function  HasArrayItem(const name: string): boolean; overload;
3 function  HasArrayItem(const param: TOmniValue): boolean; overload;

In Delphi 2010 and newer TOmniValue also implements functions for converting data to and from TArray<T> for any supported type. CastFrom<T> and CastTo<T> functions are used internally to do the conversion.

1 class function FromArray<T>(const values: TArray<T>): TOmniValue; static;
2 function  ToArray<T>: TArray<T>;

1.8.7 Handling Records

A record T can be stored inside a TOmniValue by calling the FromRecord<T> function. To extract the data back into a record, use the ToRecord<T> function.

1 class function FromRecord<T: record>(const value: T): TOmniValue; static;
2 function  ToRecord<T>: T;

An example:

1 var
2   ts: TTimeStamp;
3   ov: TOmniValue;
4   
5 ov := TOmniValue.FromRecord<TTimeStamp>(ts);
6 ts := ov.ToRecord<TTimeStamp>;

TOmniValue jumps through quite some hoops to store a record. It is first converted into a TOmniRecordWrapper which is then wrapped inside an IOmniAutoDestroyObject interface to provide a reference-counted lifetime management.

Because of all that storing records inside TOmniValue is, although very flexible, not that fast.

1 class function TOmniValue.FromRecord<T>(const value: T): TOmniValue;
2 begin
3   Result.SetAsRecord(
4     CreateAutoDestroyObject(
5       TOmniRecordWrapper<T>.Create(value)));
6 end;

1.8.8 Object Ownership

TOmniValue can take an ownership of a TObject-type data. To achieve that, you can either assign an object to the AsOwnedObject property or set the OwnsObject property to True.

1 property  AsOwnedObject: TObject;
2 function  IsOwnedObject: boolean;
3 property OwnsObject: boolean;

When a object-owning TOmniValue goes out of scope, the owned object is automatically destroyed.

You can change the ownership status at any time by assigning to the OwnsObject property.

1.8.9 Working With TValue

In Delphi 2010 and newer, TOmniValue also provides a AsTValue property and corresponding Implicit operator so you can easily convert a TValue data into TOmniValue and back.

1 class operator Implicit(const a: TValue): TOmniValue; inline;
2 class operator Implicit(const a: TOmniValue): TValue; inline;
3 property AsTValue: TValue;

1.8.10 Low-level Methods

For people with special needs (and for internal OmniThreadLibrary use), TOmniValue exposes following public methods.

1 procedure _AddRef;
2 procedure _Release;
3 procedure _ReleaseAndClear;
4 function  RawData: PInt64;
5 procedure RawZero;

_AddRef increments reference counter of stored data if TOmniValue contains such data.

_Release decrements reference counter of stored data if TOmniValue contains such data.

_ReleaseAndClear is just a shorthand for calling a _Release followed by a call to RawZero.

RawData returns pointer to the data stored in the TOmniValue.

RawZero clears the stored data without decrementing the reference counter.

1.9 TOmniValueObj

The OtlCommon unit also implements a simple object which can wrap a TOmniValue for situations where you would like to store it inside a data structure that only supports object types.

1 TOmniValueObj = class
2   constructor Create(const value: TOmniValue);
3   property Value: TOmniValue read FValue;
4 end;

1.10 Fluent Interfaces

OmniThreadLibrary heavily uses fluent interface approach. Most of functions in OmniThreadLibrary interfaces are returning Self as the output value. Take for example this declaration of the pipeline abstraction, slightly edited for brevity.

 1 IOmniPipeline = interface
 2   procedure Cancel;
 3   function  From(const queue: IOmniBlockingCollection): IOmniPipeline;
 4   function  HandleExceptions: IOmniPipeline;
 5   function  NumTasks(numTasks: integer): IOmniPipeline;
 6   function  OnStop(const stopCode: TProc): IOmniPipeline;
 7   function  Run: IOmniPipeline;
 8   function  Stage(
 9     pipelineStage: TPipelineSimpleStageDelegate; 
10     taskConfig: IOmniTaskConfig = nil): IOmniPipeline; overload;
11   function  Stage(
12     pipelineStage: TPipelineStageDelegate; 
13     taskConfig: IOmniTaskConfig = nil): IOmniPipeline; overload;
14   function  Stage(
15     pipelineStage: TPipelineStageDelegateEx; 
16     taskConfig: IOmniTaskConfig = nil): IOmniPipeline; overload;
17   function  Stages(
18     const pipelineStages: array of TPipelineSimpleStageDelegate;
19     taskConfig: IOmniTaskConfig = nil): IOmniPipeline; overload;
20   function  Stages(
21     const pipelineStages: array of TPipelineStageDelegate; 
22     taskConfig: IOmniTaskConfig = nil): IOmniPipeline; overload;
23   function  Stages(
24     const pipelineStages: array of TPipelineStageDelegateEx; 
25     taskConfig: IOmniTaskConfig = nil): IOmniPipeline; overload;
26   function  Throttle(numEntries: integer; unblockAtCount: integer = 0): 
27     IOmniPipeline;
28   function  WaitFor(timeout_ms: cardinal): boolean;
29   property Input: IOmniBlockingCollection read GetInput;
30   property Output: IOmniBlockingCollection read GetOutput;
31 end;

As you can see, most of the functions return the IOmniPipeline interface. In code, this is implemented by returning Self.

1 function TOmniPipeline.From(
2   const queue: IOmniBlockingCollection): IOmniPipeline;
3 begin
4   opInput := queue;
5   Result := Self;
6 end;

This allows calls to such interfaces to be chained. For example, the following code from the Pipeline section shows how to use Parallel.Pipeline without ever storing the resulting interface in a variable.

 1 var
 2   sum: integer;
 3 
 4 sum := Parallel.Pipeline
 5   .Stage(
 6     procedure (const input, output: IOmniBlockingCollection)
 7     var
 8       i: integer;
 9     begin
10       for i := 1 to 1000000 do
11         output.Add(i);
12     end)
13   .Stage(
14     procedure (const input: TOmniValue; var output: TOmniValue)
15     begin
16       output := input.AsInteger * 3;
17     end)
18   .Stage(
19     procedure (const input, output: IOmniBlockingCollection)
20     var
21       sum: integer;
22       value: TOmniValue;
23     begin
24       sum := 0;
25       for value in input do
26         Inc(sum, value);
27       output.Add(sum);
28     end)
29   .Run.Output.Next;

If you don’t like fluent interface approach, don’t worry. OmniThreadLibrary can be used without it. You can always call a function as if it is a procedure and compiler will just throw away the result.

The example above could be rewritten as such.

 1 var
 2   sum: integer;
 3   pipe: IOmniPipeline;
 4 
 5 pipe := Parallel.Pipeline;
 6 pipe.Stage(
 7   procedure (const input, output: IOmniBlockingCollection)
 8   var
 9     i: integer;
10   begin
11     for i := 1 to 1000000 do
12       output.Add(i);
13   end);
14 pipe.Stage(
15   procedure (const input: TOmniValue; var output: TOmniValue)
16   begin
17     output := input.AsInteger * 3;
18   end);
19 pipe.Stage(
20   procedure (const input, output: IOmniBlockingCollection)
21   var
22     sum: integer;
23     value: TOmniValue;
24   begin
25     sum := 0;
26     for value in input do
27       Inc(sum, value);
28     output.Add(sum);
29   end);
30 pipe.Run;
31 sum := pipe.Output.Next;