OmniThreadLibrary is a multi-threading library for Delphi, written mostly by the author of this book (see Credits for a 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. In some ways it is similar to ITask
which was introduced in Delphi XE7 except that OmniThreadLibrary’s implementation offers more rounded feature set.
Thirdly, OmniThreadLibrary introduces high-level multithreading concept. High-level framework contains multiple pre-packaged solutions (so-called abstractions) which can be used in your code. The idea is that the user should just choose appropriate abstraction (Future, Pipeline, Map …) and write the worker code, while the OmniThreadLibrary provides the framework that implements the tricky multi-threaded parts, takes care of synchronisation and handles other menial tasks.
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. Delphi XE or newer is recommended as some parts of the framework aren’t supported in Delphi 2009 and 2010 due to compiler bugs.
OmniThreadLibrary only works in Windows applications. Both 32-bit and 64-bit platform are supported. Applications can be compiled with the VCL library, as a service or as a console application. FireMonkey is currently not supported.
OmniThreadLibrary is an open-sourced library with the OpenBSD license.
This software is distributed under the BSD license.
Copyright (c) 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:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- The name of the Primoz Gabrijelcic may not be used to endorse or promote products derived from this software without specific prior written permission.
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 simpler words, the license says:
In case your company would like to get a support contract for the OmniThreadLibrary, please contact me.
uses
statement and start using the library!Delphi/RAD Studio XE8 and newer come with an integrated package manager called GetIt.
With GetIt you can install OmniThreadLibrary with just a few clicks. Start Delphi and open Tools, GetIt Package Manager. Enter ‘omnithreadlibrary’ into the search bar. Then click on the INSTALL button under the OmniThreadLibrary graphics.
GetIt will download OmniThreadLibrary from Embarcadero’s servers, add source path to the Library path and compile and install the design package.
To find the demos, look at the Library path. It will contain something like this at the end: $(BDSCatalogRepository)\OmniThreadLibrary_3.07.1-Tokyo\src\
. To find the true path, look into Tools, Options, Environment Options, Environment Variables where BDSCatalogRepository
is defined.
Removing OmniThreadLibrary from Delphi is equally simple. Just open GetIt, select the Installed category and click the UNINSTALL button under the OmniThreadLibrary graphics.
Delphinus is a 3rd party package manager for Delphi XE and newer, similar to Embarcadero’s own GetIt package manager.
Unlike GetIt, which comes integrated into Delphi, you have to install Delphinus manually. Recommended installation procedure is:
Once Delphinus is installed, click the Tools, Delphinus and in the Delphinus window click the green Refresh icon. When the package list is refreshed, enter ‘omnithreadlibrary’ into the search bar and press <Enter>. Click on the OmniThreadLibrary list item to get additional description in the panel on the right.
To install OmniThreadLibrary, click the Install icon (blue arrow pointing downwards to the disk drive). Be patient as Delphinus may take some time without displaying any progress on the screen.
When installation is complete, click the Show log button and in the log find the path where OmniThreadLibrary was installed (look for Adding libpathes message). Inside that folder you’ll also find all OmniThreadLibrary demos.
K> You can find this path in Delphi’s Library path configuration setting.
Delphinus will compile and install appropriate package so everything is set up for you.
Removing OmniThreadLibrary from Delphi is equally simple. Just open Delphinus, select the Installed category, select OmniThreadLibrary and click the Remove icon (red circle with white X).
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 demo applications.
If you installed OmniThreadLibrary with GetIt or Delphinus the installation process has already installed the design package and you may omit this step.
To compile and install the package containing this component, follow these steps:
TOmniEventMonitor
component was installed.You should repeat these steps whenever the OmniThreadLibrary installation is updated.
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 multi-threading.
OmniThreadLibrary was designed to become a “VCL for multi-threading” – a library that will make typical multi-threading tasks really simple but still allow you to dig deeper and mess with the multi-threading code at the operating system level. While still allowing this low-level tinkering, OmniThreadLibrary enables 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 multi-threaded code into reusable components (classes and high-level abstractions), OmniThreadLibrary allows you to write better multithreaded code faster.
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 a 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 a 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 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.
I believe that locking is evil. It leads to slow code and deadlocks and is one of the main reasons for almost-working multi-threaded 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 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 micro-locked) 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.
Because of 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 thread from a console application requires more work. Additional work is also required if you are creating OmniThreadLibrary threads from background threads.
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 critical to process messages. The program would continue to function correctly even when message processing is removed. In other cases, however, all kinds of weird behaviour can occur if messages are not processed. 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.
Similar considerations take order when an OmniThreadLibrary task is started from another OmniThreadLibrary task. The intermediate task (the task which starts another task) must process messages. The easiest way to achieve that is by using the MsgWait
qualifier when creating a task.
In the 66_ThreadsInThreads
demo, the click on the OTL from an OTL task 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 important part of this demo 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 in a background worker thread. It may look complicated, however, the code simply 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
takes a message that was sent from the future, adds some text and current thread ID and sends new 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 simply 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).
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.
Enhancing a basic Delphi TThread
is easy with the OmniThreadLibrary and takes only a few simple steps. We have to make sure that any thread messages are periodically processed 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.
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
2 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 time 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 a similar behaviour as in the previous example.
A TOmniValue
(part of the OtlCommon unit) is a data type 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 implemented as a smart record (a record with functions and operators) which functions similarly to a Variant
or TValue
but is faster3. 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 a 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.
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, the getter for the AsString
property is called CastToString
. Internally it calls TryCastToString
, which is 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 manner, 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
;
Function LogValue
[3.07.6] returns a string containing both the type of the stored data and stored value.
1
function
LogValue
:
string
;
This function is useful for data logging and debugging. See the source code for details.
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
;
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
;
Calling Clear
is slightly faster.
TOmniValue
also implements several Implicit
operators which help with automatic conversion to and from different data types. Internally, they are implemented as an 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.
Few methods simplify using TOmniValue
with class and record data.
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 simple types (integer, object) are supported. Starting with Delphi 2010, 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>
. Since [3.07.7] CastTo<T>
supports casting into an interface on Delphi XE3 and newer.
CastToObject<T>
(available in Delphi 2010 and newer) performs a hard cast with no type checking. It is equivalent to using T(omnivalue.AsObject)
ToObject<T>
(available in Delphi 2010 and newer) casts the object to type T
with type checking. It is equivalent to using omnivalue.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>()
. Trailing ()
is required.
Wrap
and Unwrap
are especially useful as they allow you to store TMethod
data (event handlers) in a TOmniValue
variable.
Each TOmniValue
can contain an array of other TOmniValue
s. 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
;
Starting with Delphi 2010 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
>;
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 that convoluted process, storing records inside TOmniValue
is 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
;
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 an object-owning TOmniValue
goes out of scope, it automatically destroys the owned object.
You can change the ownership status at any time by setting the OwnsObject
property.
Starting with Delphi 2010 TOmniValue
provides an AsTValue
property and corresponding Implicit
operator so you can easily convert a TValue
data into a 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
;
For programmers with special requirements (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 a pointer to the data stored in the TOmniValue
.
RawZero
clears the stored data without decrementing the reference counter.
The OtlCommon unit 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
;
OmniThreadLibrary heavily uses fluent interface approach. Most of the functions in OmniThreadLibrary interfaces are returning Self
as the result. 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
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 of the book 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
;