Monday, 5 December 2016

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:
  • bounded (size-limited) stack
  • bounded (size-limited) queue
  • message queue
  • dynamic (growing) queue
  • blocking collection
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 TOmniValue

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 faster. It can store following data types:
  • simple values (byte, integer, char, double, …)
  • strings (Ansi, Unicode)
  • Variant
  • objects
  • interfaces
  • records (in D2009 and newer)
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.7.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.
property AsAnsiString: AnsiString;
property AsBoolean: boolean;
property AsCardinal: cardinal;
property AsDouble: Double;
property AsDateTime: TDateTime;
property AsException: Exception;
property AsExtended: Extended;
property AsInt64: int64 read;
property AsInteger: integer;
property AsInterface: IInterface;
property AsObject: TObject;
property AsOwnedObject: TObject;
property AsPointer: pointer;
property AsString: string;
property AsVariant: Variant;
property AsWideString: WideString;

Exceptions can be stored through the AsObject property, but there’s also a special support for Exception data type with its own data access property AsException. It is extensively used in the Pipeline high-level abstraction.

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.
function TOmniValue.CastToString: string;
begin
if not TryCastToString(Result) then
raise Exception.Create('TOmniValue cannot be converted to string');
end;
function TOmniValue.TryCastToString(var value: string): boolean;
 begin
 Result := true;
   case ovType of
     ovtNull:       value := '';
     ovtBoolean:    value := BoolToStr(AsBoolean, true);
     ovtInteger:    value := IntToStr(ovData);
     ovtDouble,
     ovtDateTime,
     ovtExtended:   value := FloatToStr(AsExtended);
     ovtAnsiString: value := string((ovIntf as IOmniAnsiStringData).Value);
     ovtString:     value := (ovIntf as IOmniStringData).Value;
     ovtWideString: value := (ovIntf as IOmniWideStringData).Value;
     ovtVariant:    value := string(AsVariant);
     else Result := false;
   end;
 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.
function  TryCastToAnsiString(var value: AnsiString): boolean;
 function  TryCastToBoolean(var value: boolean): boolean;
 function  TryCastToCardinal(var value: cardinal): boolean;
 function  TryCastToDouble(var value: Double): boolean;
 function  TryCastToDateTime(var value: TDateTime): boolean;
 function  TryCastToException(var value: Exception): boolean;
 function  TryCastToExtended(var value: Extended): boolean;
 function  TryCastToInt64(var value: int64): boolean;
 function  TryCastToInteger(var value: integer): boolean;
 function  TryCastToInterface(var value: IInterface): boolean;
 function  TryCastToObject(var value: TObject): boolean;
 function  TryCastToPointer(var value: pointer): boolean;
 function  TryCastToString(var value: string): boolean;
 function  TryCastToVariant(var value: Variant): boolean;
 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.
function  CastToAnsiStringDef(const defValue: AnsiString): AnsiString;
 function  CastToBooleanDef(defValue: boolean): boolean;
 function  CastToCardinalDef(defValue: cardinal): cardinal;
 function  CastToDoubleDef(defValue: Double): Double;
 function  CastToDateTimeDef(defValue: TDateTime): TDateTime;
 function  CastToExceptionDef(defValue: Exception): 
 Exception;
 function  CastToExtendedDef(defValue: Extended): Extended;
 function  CastToInt64Def(defValue: int64): int64;
 function  CastToIntegerDef(defValue: integer): integer;
 function  CastToInterfaceDef(const defValue: IInterface): IInterface;
 function  CastToObjectDef(defValue: TObject): TObject;
 function  CastToPointerDef(defValue: pointer): pointer;
 function  CastToStringDef(const defValue: string): string;
 function  CastToVariantDef(defValue: Variant): Variant;
 function  CastToWideStringDef(defValue: WideString): WideString;

They are all implemented in the same value, similar to the CastToObjectDef below.
function TOmniValue.CastToObjectDef(defValue: TObject): TObject;
 begin
   if not TryCastToObject(Result) then
     Result := defValue;
 end;

0 comments:

Post a Comment