Thursday, July 17, 2008

OmniThreadLibrary progress report

WiPJust a short notice on what we're working on. I'm too tired to write a coherent article, longer than 100 words.

  • Lock-free structures are now really, really, really working.
  • Stress tests in tests\10_Containers have been significantly enhanced.
  • New Counter support.

The last item is really interesting and deserves a longer post. In short, it allows you to do:

counter := CreateCounter(2);

CreateTask(worker).WithCounter(counter).Run;

CreateTask(worker).WithCounter(counter).Run;

In worker code:

if Task.Counter.Decrement = 0 then

  Task.Comm.Send(MSG_ALL_DONE);

Decrement is interlocked, of course.

 

All that available in the repository and as a snapshot.

Tuesday, July 15, 2008

Implementing lock-free stack

[I'm cheating. The title should be “OmniThreadLibrary internals - OtlContainers”, but I wanted to attract more readers.Winking]

WiPToday I'll be describing the OtlContainers unit. Part of that unit, actually, as there is lot to tell and lot to show. I'll focus on the lock-free stack today and describe other stuff from the same unit in a day or two.

To give the credit where it is due — most of the code you'll see today was written by a Slovenian developer ‘GJ’. His is the implementation of the lock-free stack and all the assembler parts. I only wrapped them in higher-level structures. GJ, thanks for your contribution!

If you want to follow this article with the OtlContainers loaded in IDE, please make sure that you have the latest source [quick links: snapshot, repository, OtlContainers.pas].

The topic of today's presentation is TOmniBaseContainer. This is a base class that implements lock-free size-limited stack. Multiple simultaneous writers are supported, as are multiple readers. Although this is a fully functional class, you would usually want to use more feature-rich descendant TOmniStack.

A note regarding the limited size. It is much simpler to implement size-limited lock-free structure than to deal with dynamic allocation. In most cases, dynamic allocation would lead to possible locks inside the memory manager and some of the advantage gained by the lock-free algorithm would go away. Even more, you usually don't want the lock-free structure to grow without any control. Sure, OTL implementation won't be the right solution to all your needs but still we do believe that it is good enough for many usage scenarios.

Back to the story ... TOmniBaseContainer stores data into TOmniLinkedData nodes. First four bytes in the node point to the next node and other bytes (as many as the application requested) contain application data. Only first of those bytes is actually included in the TOmniLinkedData structure. It is only used as a placeholder for address calculation in Push and Pop operations.

type
POmniLinkedData = ^TOmniLinkedData;
TOmniLinkedData = packed record
Next: POmniLinkedData;
Data: byte; //user data, variable size
end; { TLinkedOmniData }

In the following diagrams I'll use this symbol to represent one TOmniLinkedData node.

data node

Nodes are connected into chains. Address of the first node is stored in a chain header. The Next field of the last node in chain contains value nil

data chain

As the TOmniBaseContainer implementation is size-limited, it can preallocate all nodes in one go. The Initialize method first allocates one big buffer to store all nodes and connects them into one chain, pointed to by the recycle chain header. This chain contains unused nodes. The other chain, public chain, contains nodes that are in use and is initialized to nil.

You'll see that the initialization code rounds up the size of node data to the nearest multiplier of 4. This is required because operations that modify node pointers (Next field) require them to be aligned on addresses that are divisible by 4.

  // calculate element size, round up to next 4-aligned value
bufferElementSize := ((SizeOf(POmniLinkedData) + elementSize) + 3) AND NOT 3;
GetMem(obcBuffer, bufferElementSize * cardinal(numElements));
Assert(cardinal(obcBuffer) AND 3 = 0);
//Format buffer to recycleChain, init orbRecycleChain and orbPublicChain.
//At the beginning, all elements are linked into the recycle chain.
obcRecycleChain := obcBuffer;
nextElement := nil; // to remove compiler warning in nextElement.Next := nil assignment below
currElement := obcRecycleChain;
for iElement := 0 to obcNumElements - 2 do begin
nextElement := POmniLinkedData(cardinal(currElement) + bufferElementSize);
currElement.Next := nextElement;
currElement := nextElement;
end;
nextElement.Next := nil; // terminate the chain
obcPublicChain := nil;

Structure, created by the initialization code is depicted below. Recycle chain points to the first node, which points to the next node and so on. The last node in the chain has nil stored in the Next pointer. Public chain is empty.

empty stack

Let's see what happens when code pushes new data onto the stack.

function TOmniBaseContainer.Push(const value): boolean;
var
linkedData: POmniLinkedData;
begin
linkedData := PopLink(obcRecycleChain);
Result := assigned(linkedData);
if not Result then
Exit;
Move(value, linkedData.Data, ElementSize);
PushLink(linkedData, obcPublicChain);
end; { TOmniBaseContainer.Push }

PopLink removes first node from the recycle chain. Chain header is updated to point to the next node in the chain. Pointer to the removed node is stored in the linkedData variable. If recycle chain is empty, linkedData contains nil and Push returns False.

The code next moves application data from the value parameter to the preallocated buffer. linkedData.Data conveniently addresses first data byte in the buffer.

At the end, the code inserts linkedData node at the beginning of the public chain and updates the public chain header to point to the linkedData node.

one pushed

The real workhorses here are PopLink and PushLink. Former pops first node from a chain and latter inserts the node into the head of the chain. The trick here is that they are written with multithreading in mind — they both expect that data structures may change at any time because another thread may be accessing the structure simultaneously.

Let's take a look at the PopLink first.

function TOmniBaseContainer.PopLink(var chainHead: POmniLinkedData): POmniLinkedData;
//nil << Link.Next << Link.Next << ... << Link.Next
//FILO buffer logic ^------ < chainHead
asm
mov eax, [edx] //Result := chainHead
@Spin:
test eax, eax
jz @Exit
mov ecx, [eax] //ecx := Result.Next
lock cmpxchg [edx], ecx //chainHead := Result.Next
jnz @Spin //Do spin ???
@Exit:
end; { TOmniBaseContainer.PopLink }

At beginning, edx register contains the address of the chainHead parameter. mov eax, [edx] moves chainHead, i.e. the node that chainHead is pointing to, into the eax. Chain may be empty, in which case chainHead is nil, or in assembler terms, eax is 0. test eax, eax checks for this condition.

If there's at least one node in the chain, we can continue. mov ecx, [eax] moves the address of the next node into the ecx register. Remember that eax points to the node and [eax] is the same as accessing the node's Next field.

Now we have an address of the first node in the eax register, address of the second node in the ecx register (there may be no second node, in which case the ecx is 0) and chainHead in the edx register. And now we can do the heavy magic.

lock cmpxchg [edx], ecx does few things, all at once. Well, not exactly in once, but processor (and level one cache and all weird hardware wrapping the CPUs) makes sure that this operation won't be interrupted by another core or CPU attempting to do the same thing at the same time. Firstly, cmpxchg compares eax with [edx]. Remember, we loaded eax from [edx] so those two values should be the same, yes? Well, no. Between mov eax, [edx] and lock cmpxchg, thread may be interrupted and stopped and another thread may have modified the chain header by executing PopLink. That's why we have to recheck.

If eax and [edx] are still the same, ecx is loaded into [edx]. In other words, address of the second node (which may be nil) is stored into the chainHead. Because of the lock prefix, all that (testing and assignment) happens atomically. Uninterruptible. In other words, another thread can not step on our (digital) toes.

If eax and [edx] are not the same, cmpxchg loads eax from [edx]. In other words, eax is refreshed from the chainHead. If that happens, jnz @Spin instruction will jump to the @Spin label and repeat the whole procedure.

At the end we have address of the second node stored in chainHead and address of the first node stored in eax, which is fine as pointer Results are returned in eax.

You can read more on cmpxchg here.

PushLink is much simpler.

procedure TOmniBaseContainer.PushLink(const link: POmniLinkedData; var chainHead:
POmniLinkedData);
asm
mov eax, [ecx] //ecx = chainHead
@Spin:
mov [edx], eax //link := chainHead.Next
lock cmpxchg [ecx], edx //chainHead := link
jnz @Spin
end; { TOmniBaseContainer.PushLink }

At the beginning, edx contains the value of the link parameter and ecx contains the address of the chainHead parameter.

The code first loads the address of the first node in the chain into the eax register. Nil pointers are fine in this case as we will not be following (dereferencing) them.

Then the code loads this same value into the [edx]. Remember, edx contains the value of the link, therefore [edx] represents link.Next. The mov [edx], eax line sets link.Next to point to the first node in the chain. If the chain is empty, link.Next will be set to nil, which is exactly the correct thing to do.

lock cmpxchg [ecx], edx then compares eax and [ecx] to ensure that underlying data haven't changed since PushLink started its execution. If values are not equal, eax is reloaded from [ecx] and code execution continues from the @Spin label. If values are equal, edx is loaded into [ecx]. At that moment, edx still contains the value stored in the link parameter and ecx contains the address of the chainHead. In other words, chainHead is set to link. As link.Next was set to old chainHead in the previous line, we have successfully linked link at the beginning of the chain.

That's all, the hard part is over. If you understand PopLink and PushLink, everything else is simple.

When we push the second value into the stack, it is inserted at the beginning of the public chain and recycle chain points to the next free node.

two pushed

The process continues until all nodes are linked into the public chain and recycle chain is nil.

four pushed

To pop a value from the stack, TOmniBaseContainer.Pop is used. It first gets a topmost allocated node (atomically, of course). Then it moves node data into the method parameter and pushes node into the recycle chain (again, atomically).

function TOmniBaseContainer.Pop(var value): boolean;
var
linkedData: POmniLinkedData;
begin
linkedData := PopLink(obcPublicChain);
Result := assigned(linkedData);
if not Result then
Exit;
Move(linkedData.Data, value, ElementSize);
PushLink(linkedData, obcRecycleChain);
end; { TOmniBaseContainer.Pop }

One of the important things to note is that nodes are not moved around. They are allocated at the beginning and for the whole lifetime of the TOmniBaseContainer they stay immovabe. Only Next fields are modified (and of course the Data is copied) and that's why this lock-free stack implementation is extremely fast. First results indicate that it can move about 800.000 integers per second between two threads on a 1,67 GHz Core2 Duo (T2300) machine.

base container test

Next post will discuss the lock-free ring buffer that is built on top of the lock-free stack. Stay tuned!

Monday, July 14, 2008

OmniThreadLibrary progress report

  • WiP new unit OtlContainers with lock-free stack and lock-free ring buffer (both with additional limitations; details coming soon)
  • OtlComm redesigned around OtlContainers
  • new test 10_Containers, which performs basic unit test for the OtlContainers unit
  • package split to designtime/runtime; OtlComm unit test split into separate unit OtlCommBufferTest; component registraion moved from OtlTaskEvents to new unit OtlRegister [all thanks to Lee_Nover]
  • fixed memory leaks in tests 8 and 9
  • added demonstration of how to send an object over the communication channel to the demo 8
  • new SpinLock unit which fixes reentrancy problems is included
  • new DSiWin32 unit which fixes problems when DSiTimeGetTime64 was called from more than one thread

All that available in the repository and as a snapshot.

Friday, July 11, 2008

OmniThreadLibrary goes lock-free

WiPWell, not complete OTL, just the messaging subsystem, but even that is quite some achievement. The ring buffer inside the OtlComm unit is now implemented with a help of a lock-free stack. Lock-free buffer is now a default. If you want to compile OtlComm with the locking buffer, define the OTL_LockingBuffer symbol.

Lock-free buffer did not improve communication subsystem speed much at the moment as the limiting factor is Delphi's Variant implementation but that will change. The collective mind behind the OTL is working on some interesting messaging ideas that will be much faster but will still keep most of the Variant flexibility and simplicity.

What I can promise even now is that there will be lock-free stack and lock-free ring buffer included in the OTL as standalone reusable data structures. Sounds useful?

Wednesday, July 09, 2008

OmniThreadLibrary Example #5: Registering additional communication channels

WIPI thought I would be documenting the TOmniTaskEventDispatch component today, but I was working on something interesting yesterday and today and I want to show it to the public. The functionality I'll be talking about is demoed in the tests\8_RegisterComm project, available in the repository and in the today's snapshot. [All new functionality was uploaded, not just this demo, of course.]

I was working on the OtlComm testing and benchmarking code. I wanted to find out if the communication code is working correctly (by stress-testing it) and if the lock-free buffer implementation is faster than the locking one. So I set up two threaded clients (using OTL, of course!) and started to write code that would establish a direct communication channel between them. I wanted to use the standard Comm channel for test control and reporting, but not for running tests.

At first, the task seemed quite simple. I set up a new communication channel of type IOmniTwoWayChannel and created two tasks, based on the TOmniWorker class. Each task received one endpoint of the new communication channel as a parameter.

  FCommChannel := CreateTwoWayChannel(1024);
FClient1 := OmniTaskEventDispatch1.Monitor(
CreateTask(TCommTester.Create(FCommChannel.Endpoint1, 1024))).Run;
FClient2 := OmniTaskEventDispatch1.Monitor(
CreateTask(TCommTester.Create(FCommChannel.Endpoint2, 1024))).Run;

An image is worth more than thousand words. additional communication channel


When I had this infrastructure ready, I started to think about message loop in my tasks. At that moment (yesterday), internal message loop only handled the default communication channel. Writing a whole message loop to support one measly communication channel didn't seem like a right decision. What to do? Extend OTL, of course!


Today's snapshot contains support for additional communication channels. You only have to call RegisterComm and internal message dispatcher will handle everything for you.


The 8_RegisterComm demo will help you understand. There are two buttons on the form, first sends a random number to task 1 and second sends a random number to task 2.


8_RegisterComm


A code for first button's OnClick event should suffice. Code for the second button is almost the same.

procedure TfrmTestOtlComm.btnSendTo1Click(Sender: TObject);
var
value: integer;
begin
value := Random(100);
Log(Format('Sending %d to task 1', [value]));
FClient1.Comm.Send(MSG_FORWARD, value);
end;

The TCommTester class implements message handler for the MSG_FORWARD message. The code in this method firstly notifies the owner that MSG_FORWARD message was received and secondly sends MSG_FORWARDING message to the task-to-task communication channel.

type
TCommTester = class(TOmniWorker)
strict private
ctComm : IOmniCommunicationEndpoint;
ctCommSize: integer;
public
constructor Create(commEndpoint: IOmniCommunicationEndpoint; commBufferSize: integer);
function Initialize: boolean; override;
procedure OMForward(var msg: TOmniMessage); message MSG_FORWARD;
procedure OMForwarding(var msg: TOmniMessage); message MSG_FORWARDING;
end; { TCommTester }

constructor TCommTester.Create(commEndpoint: IOmniCommunicationEndpoint; commBufferSize:
integer);
begin
inherited Create;
ctComm := commEndpoint;
ctCommSize := commBufferSize;
end;

function TCommTester.Initialize: boolean;
begin
Task.RegisterComm(ctComm);
Result := true;
end;

procedure TCommTester.OMForward(var msg: TOmniMessage);
begin
Task.Comm.Send(MSG_NOTIFY_FORWARD, msg.MsgData);
ctComm.Send(MSG_FORWARDING, msg.MsgData);
end;

procedure TCommTester.OMForwarding(var msg: TOmniMessage);
begin
Task.Comm.Send(MSG_NOTIFY_RECEPTION, msg.MsgData);
end;

The MSG_FORWARDING handler (OMForwarding) just notifies the owner that the message was received from another task.


In the screenshot above, you can see how value 0 traveled from task 1 to task 2 and how value 3 traveled from task 2 to task 1.


The really magical part happens in TCommTester.Initialize. The code fragment Task.RegisterComm(ctComm) registers communication endpoint as an additional message source. From that point onwards, messages that arrive on the ctComm channel are treated same way as messages arriving on the Comm channel.


The road to this deceptively simple solution was quite long. I had to do major refactoring in the OtlTask unit. Quite some code was moved from the TOmniTaskControl class to the TOmniTaskExecutor class (which was a record before). I think that the new code works fine (I've run all test cases), but only time will tell ...


If you feel the need to check how additional communication channel support is implemented, check TOmniTaskExecutor. Asy_DispatchMessages and TOmniTaskExecutor.Asy_RegisterComm (OtlTask unit).

Tuesday, July 08, 2008

OmniThreadLibrary internals - OtlComm

WIP

Today I'll describe the inner workings of the OmniThreadLibrary communication subsystem. It lives in the OtlComm unit and is used extensively by the task control/task worker interfaces (described in the previous installment). Its use is not limited to the OTL as it has no knowledge of tasks and threads. Feel free to use it in your own non-OTL-related code.

At the surface, messaging subsystem looks deceptively simple.

image

IOmniTaskControl interface exposes property Comm: IOmniCommunicationEndpoint.

  IOmniCommunicationEndpoint = interface ['{910D329C-D049-48B9-B0C0-9434D2E57870}']
function GetNewMessageEvent: THandle;
//
procedure RemoveMonitor;
procedure Send(msgID: word; msgData: TOmniValue); overload;
procedure Send(msgID: word; msgData: array of const); overload;
procedure Send(const msg: TOmniMessage); overload;
procedure SetMonitor(hWindow: THandle; messageWParam, messageLParam: integer);
function Receive(var msgID: word; var msgData: TOmniValue): boolean; overload;
function Receive(var msg: TOmniMessage): boolean; overload;
property NewMessageEvent: THandle read GetNewMessageEvent;
end; { IOmniTaskCommunication }

This simple interface allows the owner to send messages, receive messages and wait on a new message. A property with a same name lives on the worker side, too (in the IOmniTask interface). Both endpoints are connected so that IOmniTaskControl.Comm.Send sends message to IOmniTask.Comm and vice versa. Simple.


But when you look under the surface ...


Here be dragons!


This is the real picture of objects and interfaces living inside the OtlComm unit. Although the surface view may give you an impression that the IOmniTaskCommunication is the most important part of the communication system, in reality everything revolves around the IOmniTwoWayChannel.


image 


To understand this picture, it's best to start at the bottom left (IOW, in the rectangle just above this line). The IOmniTaskControl interface is the one that is returned from the CreateTask procedure.


IOmniTaskControl is implemented by the TOmniTaskControl object, which owns an IOmniTwoWayChannel interface, created during TOmniTaskControl initialization.

procedure TOmniTaskControl.Initialize;
begin
//...
otcCommChannel := CreateTwoWayChannel;
//...
end; { TOmniTaskControl.Initialize }

This interface is also passed to the TOmniTask object (which implements IOmniTask interface) when task is run.

function TOmniTaskControl.Run: IOmniTaskControl;
var
task: IOmniTask;
begin
//...
task := TOmniTask.Create(..., otcCommChannel, ...);
//...
end; { TOmniTaskControl.Run }

TOmniTwoWayChannel owns two ring buffers of type TOmniRingBuffer (either the locking version, which is the default at the moment, or lock-free version if you compile the unit with /dOTL_LockFreBuffer). Those buffers are used as unidirectional message queues that store TOmniMessage records.

  TOmniMessage = record
MsgID : word;
MsgData: TOmniValue;
end; { TOmniMessage }

TOmniTwoWayChannel also owns two IOmniCommunicationEndpoint interfaces (implement by the TOmniCommunicationEndpoint class). One of them is exposed via the Enpoint1 property and another via Endpoint2.

  IOmniTwoWayChannel = interface ['{3ED1AB88-4209-4E01-AA79-A577AD719520}']
function Endpoint1: IOmniCommunicationEndpoint;
function Endpoint2: IOmniCommunicationEndpoint;
end; { IOmniTwoWayChannel }

Both endpoints are connected to both ring buffers. If we name those ring buffers A and B, endpoint 1 writes to A and reads from B while endpoint 2 writes to B and reads from A.


TOmniTaskControl.Comm and TOmniTask.Comm are simple mappers that return different endpoints of the same IOmniTwoWayChannel interface.

function TOmniTaskControl.GetComm: IOmniCommunicationEndpoint;
begin
Result := otcCommChannel.Endpoint1;
end; { TOmniTaskControl.GetComm }

function TOmniTask.GetComm: IOmniCommunicationEndpoint;
begin
Result := otCommChannel.Endpoint2;
end; { TOmniTask.GetComm }

And that's all folks ...


Autovivification


Well, that's almost all. The last trick in the OtlComm is creation of communication infrastructure on as-needed basis. As you can see from the diagram, the whole system is quite "heavy". If you're running a simple fire-and-forget tasks, you may not need it at all. That's why the ring buffers and communication endpoints are not created until they are used.


The TOmniTwoWayChannel object is created whenever a task is created (see TOmniTaskControl.Initialize, above), but it does not create other parts of the infrastructure. This is only done in the endpoint accessor.

procedure TOmniTwoWayChannel.CreateBuffers;
begin
if twcUnidirQueue[1] = nil then
twcUnidirQueue[1] := TOmniRingBuffer.Create(twcMessageQueueSize);
if twcUnidirQueue[2] = nil then
twcUnidirQueue[2] := TOmniRingBuffer.Create(twcMessageQueueSize);
end; { TOmniTwoWayChannel.CreateBuffers }


function
TOmniTwoWayChannel.Endpoint1: IOmniCommunicationEndpoint;
begin
Assert((cardinal(@twcEndpoint[1]) AND 3) = 0);
if twcEndpoint[1] = nil then begin
twcLock.Acquire;
try
if twcEndpoint[1] = nil then begin
CreateBuffers;
twcEndpoint[1] := TOmniCommunicationEndpoint.Create(twcUnidirQueue[1], twcUnidirQueue[2]);
end;
finally twcLock.Release; end;
end;
Result := twcEndpoint[1];
end; { TOmniTwoWayChannel.Endpoint1 }

The code to create Endpoint2 is similar except that it uses twcEndpoint[2] and reverses parameters passed to the TOmniCommunicationEndpoint.Create.


The Endpoint1 method uses some tricks that may not be obvious.



  • Testing if interfaces have been already initialized is optimistic. The code first tests if endpoint is nil and if that is true (and that will be very rarely, only the first time), it locks access to internal structures and the retests the same condition before creating buffers and the endpoint.
  • The Assert checks if two least important bits of the endpoint address are 0. This makes the endpoint variable DWORD-aligned (its address is divisible by 4), which in turn causes reads/writes from/to that address to be atomic on the Intel architecture (the only one that concerns us). In other words, even if another thread is modifying the same variable on another CPU (or core), we know that we will read either the old value or the new value and not some mixture of both.

That concludes the OtlComm tour. The only remaining part (until the thread pool is implemented) is the TOmniTaskEventDispatch component, which I'll cover in a day or two.

Monday, July 07, 2008

OmniThreadLibrary lock-free communication

image image I've updated the OtlComm.pas unit in OTL home page with a lock-free circular buffer, donnated by a fellow Slovenian Delphi programmer who goes by a nick 'GJ'.

This code is doubly-work-in-progress! I haven't even test it. It is disabled by default and you have to define conditional symbol LockFreeBuffer to use it.

If you'll be making any comparisons to my original communication code and especially if you find any problem with new code, let me know.

UPDATE: I decided to use OTL_LockFreeBuffer instead of LockFreeBuffer. Repository has been updated, too.

Friday, July 04, 2008

OmniThreadLibrary internals - OtlTask

image [Please note - new snapshot is available on the OTL home page.]

In next few posts, I'll try to present a rough overview of the OTL internals.

The most important unit is OtlTask. It hosts interfaces describing threaded task. Currently, it also provides thread descendant to run those tasks, but that one will most probably be moved to the OtlThread unit.

The most important interface in OtlTask is IOmniTaskControl. This task control interface is returned from the CreateTask function. It is used to communicate with the task and to control its behavior. Four CreateTask functions map directly to four constructors, which store execution parameters into internal field and initialize some events used for task control.

    constructor Create(worker: IOmniWorker; const taskName: string); overload;
constructor Create(worker: TOmniWorker; const taskName: string); overload;
constructor Create(executor: TOmniTaskMethod; const taskName: string); overload;
constructor Create(executor: TOmniTaskProcedure; const taskName: string); overload;

SetParameter* methods are used to set parameters that are passed to the threaded worker.

    function  SetParameter(const paramName: string; paramValue: TOmniValue): IOmniTaskControl; overload;
function SetParameter(paramValue: TOmniValue): IOmniTaskControl; overload;
function SetParameters(parameters: array of TOmniValue): IOmniTaskControl;

Next four methods are used to control internal event/message loop behavior. They are only used when working with IOmniWorker or TOmniWorker tasks. Alertable causes message wait to be alertable and MsgWait causes message wait to process messages (see TOmniTaskControl.DispatchMessages for more information). SetTimer causes timerMessage message to be sent to the worker in specified intervals. If timerMessage is -1, worker's Timer method is called instead. FreeOnTerminate is only available when worker is a descendant of the TOmniWorker class and causes the worker to be destroyed after the task is completed.

    function  Alertable: IOmniTaskControl;
function FreeOnTerminate: IOmniTaskControl;
function MsgWait(wakeMask: DWORD = QS_ALLEVENTS): IOmniTaskControl;
function SetTimer(interval_ms: cardinal; timerMessage: integer = -1): IOmniTaskControl;

SetMonitor and RemoveMonitor are used to attach external message watching monitor. Typically that would be the TOmniTaskEventDispatch component and those TOmniTaskControl messages would be called component's Monitor and Detach functions.

    function  RemoveMonitor: IOmniTaskControl;
function SetMonitor(hWindow: THandle): IOmniTaskControl;

To start the task, you must execute either Run (creates a new thread and starts executing the task) or Schedule (schedules task to be executed in a thread pool; not implemented yet).

    function  Run: IOmniTaskControl;
function Schedule(threadPool: IOmniThreadPool = nil {default pool}): IOmniTaskControl;

There are four other methods related to the execution control. Terminate stops the task and waits the specified amount of time for the task to complete gracefully. At the moment it returns False if task does not terminate in the allotted time. In the future, it will maybe kill the thread. I'm not sure yet.


TerminateWhen is not implemented yet. It was supposed to assign another synchronization object to the thread. That object would cause the task to stop. The idea was to use one event to stop multiple tasks at once, but I'm not sure if that is useful at all.


WaitFor waits for the task to finish execution and returns True if task has stopped. WaitForInit waits for IOmniWorker/TOmniWorker worker to execute its Initialize method and returns initialization status (return value of the Initialize method).

    function  Terminate(maxWait_ms: cardinal = INFINITE): boolean;
function TerminateWhen(event: THandle): IOmniTaskControl;
function WaitFor(maxWait_ms: cardinal): boolean;
function WaitForInit: boolean;

The Comm property gives the owner access to the communication subsystem. This will be covered in a separate post.


ExitCode and ExitMessage will return task's exit code and message. Not implemented yet.


Name returns task's name, as set in the constructor and UniqueID returns task's unique ID. This identifier is unique inside the application.


Options gives you direct access to execution options, usually set with Alertable, MsgWait, and FreeOnTerminate.

    property Comm: IOmniCommunicationEndpoint read GetComm;
property ExitCode: integer read GetExitCode;
property ExitMessage: string read GetExitMessage;
property Name: string read GetName;
property Options: TOmniTaskControlOptions read otcOptions write otcOptions;
property UniqueID: cardinal read GetUniqueID;

Execution


Run first makes parameters immutable, then creates thread-side task interface (IOmniTask), creates new thread, assigns that task to the thread and starts the thread.

function TOmniTaskControl.Run: IOmniTaskControl;
var
task: IOmniTask;
begin
otcParameters.Lock;
task := TOmniTask.Create(otcExecutor, otcTaskName, otcParameters, otcCommChannel,
otcUniqueID, otcTerminateEvent, otcTerminatedEvent, otcMonitorWindow);
otcThread := TOmniThread.Create(task);
otcThread.Resume;
Result := Self;
end; { TOmniTaskControl.Run }

Thread's Execute method passes control to the TOmniTaskControl's Execute method.

procedure TOmniThread.Execute;
begin
{$IFNDEF OTL_DontSetThreadName}
SetThreadName(otTask.Name);
{$ENDIF OTL_DontSetThreadName}
(otTask as IOmniTaskExecutor).Execute;
end; { TOmniThread.Execute }

Execute calls either procedure or method worker. When working with IOmniWorker/TOmniWorker worker, Execute calls TOmniTaskControl.DispatchMessages. In all cases, IOmniTask is passed as a parameter.

procedure TOmniTaskExecutor.Execute(task: IOmniTask);
begin
case ExecutorType of
etMethod:
Method(task);
etProcedure:
Proc(task);
else
raise Exception.Create('TOmniTaskExecutor.Execute: Executor is not set');
end;
end; { TOmniTaskExecutor.Execute }

IOmniTask


IOmniTask interface allows the worker to communicate with the owner.

  IOmniTask = interface
procedure SetExitStatus(exitCode: integer; const exitMessage: string);
procedure Terminate;
property Comm: IOmniCommunicationEndpoint read GetComm;
property Name: string read GetName;
property Param[idxParam: integer]: TOmniValue read GetParam;
property ParamByName[const paramName: string]: TOmniValue read GetParamByName;
property TerminateEvent: THandle read GetTerminateEvent;
property UniqueID: cardinal read GetUniqueID;
end; { IOmniTask }

SetExitStatus sets exit code and message, which are then mapped to task control's ExitCode and ExitMessage parameters. Not implemented yet.


Terminate sets task's TerminateEvent. Thus the task can terminate itself. TerminateEvent is also set when code calls task control interface's Terminate method.


Comm returns task's communication endpoint. More details will be provided in the next post.


Name returns the task name, as set in the CreateTask call. UniqueID returns task's unique ID.


Param and ParamByName can be used to access parameters passed to the task.


More information on task writing is available in examples (and already published posts on the OTL topic).

Thursday, July 03, 2008

OmniThreadLibrary Example #4a: Bidirectional communication, with objects

imageThis is a variation of Example #4 that uses TOmniWorker object instead of IOmniWorker interface. Code is stored in folder tests\6_TwoWayHello_with_object_worker.

I strongly suggest that you look at Example #4 first, as this post only lists the differences between those two examples.

Firstly, we don't need TAsyncHello.Initialize. We'll set initial message text in the constructor. [Admittedly, this is something that can also be done when using IOmniWorker approach.] Message handlers are identical to the Example #4.

  TAsyncHello = class(TOmniWorker)
strict private
aiMessage: string;
public
constructor Create(const initialMessage: string);
procedure OMChangeMessage(var msg: TOmniMessage); message MSG_CHANGE_MESSAGE;
procedure OMSendMessage(var msg: TOmniMessage); message MSG_SEND_MESSAGE;
end;
constructor TAsyncHello.Create(const initialMessage: string);
begin
aiMessage := initialMessage;
end;

Secondly, we have to tweak the task creation.

procedure TfrmTestOTL.actStartHelloExecute(Sender: TObject);
begin
FHelloTask :=
OmniTaskEventDispatch1.Monitor(CreateTask(TAsyncHello.Create('Hello'), 'Hello')).
SetIdle(1000, MSG_SEND_MESSAGE).
SetParameter('Delay', 1000).
FreeOnTerminate.
Run;
end;

TAsyncHello object can now be passed directly to the CreateTask method. There is no need to set the Message parameter. Also new is the FreeOnTerminate call which tells the task manager to destroy the worker object when task is terminated. Alternatively, you can store away the worker object and destroy it "manually" after the FHelloTask is terminated.


The rest of the code does not differ from the Example #4.

OmniThreadLibrary Example #4: Bidirectional communication, the OTL way

[Please note - today's snapshot is available on the OTL home page.]

image Today's topic is writing message-driven tasks without having to write an event/message processing loop. Of course, the loop is still there, but it is hidden inside the OtlTask unit.

The demo is stored in folder tests\5_TwoWayHello_without_loop. OTL package is required to open the main form.

This time the worker is not a method or procedure, but a whole object. It must either implement IOmniWorker interface or be a descendant of the TOmniWorker class (which implements IOmniWorker).

The worker class will process two messages. When MSG_CHANGE_MESSAGE is received, worker will change internal message text. When MSG_SEND_MESSAGE is received, worker will send this message text to the owner (just like in yesterday's example). We'll also override the Initialize method to set initial message text. Initialize and it's counterpart Cleanup are executed in the context of the background thread executing the task so they can be used to allocate/deallocate thread-sensitive objects.

const
MSG_CHANGE_MESSAGE = 1;
MSG_SEND_MESSAGE = 2;

type
TAsyncHello = class(TOmniWorker)
strict private
aiMessage: string;
public
function Initialize: boolean; override;
procedure OMChangeMessage(var msg: TOmniMessage); message MSG_CHANGE_MESSAGE;
procedure OMSendMessage(var msg: TOmniMessage); message MSG_SEND_MESSAGE;
end;

We can write those two message handlers identical to the way we write Windows message handlers. Message IDs are arbitrary, I usually start with 1.

{ TAsyncHello }

function TAsyncHello.Initialize: boolean;
begin
aiMessage := Task.ParamByName['Message'];
Result := true;
end;

procedure TAsyncHello.OMChangeMessage(var msg: TOmniMessage);
begin
aiMessage := msg.MsgData;
end;

procedure TAsyncHello.OMSendMessage(var msg: TOmniMessage);
begin
Task.Comm.Send(0, aiMessage);
end;

Worker code (above) is trivial, task creation code only slightly less.

procedure TfrmTestOTL.actStartHelloExecute(Sender: TObject);
var
worker: IOmniWorker;
begin
worker := TAsyncHello.Create;
FHelloTask :=
OmniTaskEventDispatch1.Monitor(CreateTask(worker, 'Hello')).
SetIdle(1000, MSG_SEND_MESSAGE).
SetParameter('Delay', 1000).
SetParameter('Message', 'Hello').
Run;
end;

First we create  the worker object and store it in the interface variable. CreateTasks then takes this worker interface instead of worker method/procedure. SetParameters are same as in the previous example (except that the parameter order has been changed to name, value in today's snapshot). The only mystery here is the SetIdle call. It tells the internal event/message loop to send the MSG_SEND_MESSAGE every 1000 milliseconds. [The second parameter - message ID - is optional. When left out, worker's Idle method is called instead.]


A word of warning: SetIdle will be most probably renamed to SetTimer in the next release.


What about MSG_CHANGE_MESSAGE? It is generated in completely the same way as in the previous example.

procedure TfrmTestOTL.actChangeMessageExecute(Sender: TObject);
begin
FHelloTask.Comm.Send(MSG_CHANGE_MESSAGE, 'Random ' + IntToStr(Random(1234)));
end;

Same goes for the task termination - just call FHelloTask.Terminate and set FHelloTask to nil. Worker object will be destroyed automatically.


So, what you say? Simple enough?

Wednesday, July 02, 2008

OmniThreadLibrary Example #3: Bidirectional communication

image In the third installment of my introduction to the OmniThreadLibrary, I'll show how you can implement bidirectional communication with the threaded task. This time there will be more code to write so you should probably just open example in tests\4_TwoWayHello_with_package folder (or tests\2_TwoWayHello if you don't want to install the OTL package).

For your convenience, I've uploaded snapshot of the current repository state to the projects home address http://code.google.com/p/omnithreadlibrary/.

For the readers that are following my exposé in the browser, here is the screenshot of  today's example in action.

image

There are three buttons - first starts the threaded task, second instructs the task to change the message and thirds stops the task. There are also a TOmniTaskEventDispatch and a TActionList components on a form.

The actStart action is connected to the first button and starts the threaded task.

procedure TfrmTestOTL.actStartHelloExecute(Sender: TObject);
begin
FHelloTask :=
OmniTaskEventDispatch1.Monitor(CreateTask(RunHello, 'Hello')).
SetParameter(1000, 'Delay').
SetParameter('Hello', 'Message').
Run;
end;

The task is created in an already familiar manner (CreateTask output passed to the Monitor method). Then, two named parameters are set. First parameter to the SetParameter method contains a Variant value and second (optional) contains parameter's name.


At the end, Run is called to start the task and task control interface is stored in a field.

strict private
FHelloTask: IOmniTaskControl;

Stopping the task is trivial.

procedure TfrmTestOTL.actStopHelloExecute(Sender: TObject);
begin
FHelloTask.Terminate;
FHelloTask := nil;
end;

When you click the second button (Change message), standard communication channel (introduced in previous installment) is used to send a message containing new text.

procedure TfrmTestOTL.actChangeMessageExecute(Sender: TObject);
begin
FHelloTask.Comm.Send(MSG_CHANGE_MESSAGE, 'Random ' + IntToStr(Random(1234)));
end;

The OmniTaskEventDispatch1's event handlers are identical to the Hello, world! example.


The part that is new today is the worker code which must receive and process MSG_CHANGE_MESSAGE messages. Today, this code is a normal procedure and not a method (just to show that this option is available, too).

procedure RunHello(task: IOmniTask);
var
msg : string;
msgData: TOmniValue;
msgID : word;
begin
msg := task.ParamByName['Message'];
repeat
case DSiWaitForTwoObjects(task.TerminateEvent, task.Comm.NewMessageEvent,
false, task.ParamByName['Delay'])
of
WAIT_OBJECT_1:
begin
task.Comm.Receive(msgID, msgData);
if msgID = MSG_CHANGE_MESSAGE then
msg := msgData;
end;
WAIT_TIMEOUT:
task.Comm.Send(0, msg);
else
break; //repeat
end;
until false;
end;

RunHello first retrieves the value of the Message parameter. Then it enters an infinite loop (repeat .. until false) in which it waits for either task.TerminateEvent or task.Comm.NewMessageEvent. DSiWin32 is used for brevity only. Normal WaitForMultipleObjects API call could be used insted.


Task.TerminateEvent is signaled in the Terminate method (when user clicks the Stop "Hello" button). In this case, code simply breaks out of the repeat..until loop.


Task.Comm.NewMessageEvent is signaled when a message is waiting in the communication queue. In this case (WAIT_OBJECT_1), message is received and processed.


If nothing was signaled for <value of the 'Delay' parameter> milliseconds, the WAIT_TIMEOUT path is taken and message msg (whatever the current value may be) is sent to the owner where it is displayed.


If you did any threaded pr0gramming then you certainly recognized this loop - this is fairly standard approach when writing multithreaded code. Nothing special here except the messaging subsystem. Still, OTL doesn't stop here. As you'll see in the next example, it is possible to rewrite this code in much simpler way.

OmniThreadLibrary Example #2: Hello, world!

imageLast time I've shown how to write a trivial threaded Beep in two lines.  Today I'll do something more complicated - a threaded Hello, world program that communicates with its owner. Communication will be unidirectional; I'll leave bidirectional communication for the next example.

First, compile and install the OmniThreadLibrary_D2007 package (from the packages\ subfolder). This will add one component to your palette - TOmniTaskEventDispatch. This component simplifies communication between a threaded task and primary thread.

As before, start with an empty form and drop a button on it. You'll also need a listbox to show "Hello, world!" messages. You'll also need to drom one TOmniTaskEventDispatch component on the form.

Write an OnClick handler for the button.

procedure TfrmTestOTL.btnHelloClick(Sender: TObject);
begin
btnHello.Enabled := false;
OmniTaskEventDispatch1.Monitor(CreateTask(RunHelloWorld, 'HelloWorld')).Run;
end;

The code is quite similar to the one you had to write in the first example, with one important modification - it passes the task interface, created in CreateTask, to the TOmniTaskEventDispatch component. That will tell  the component to start monitoring messages from the threaded task.


As you can see, TOmniTaskEventDispatch.Monitor takes as a parameter the interface returned from CreateTask (I'll give more attention to this interface in latter posts) and it also returns that same interface as its result so that you can immediately call the Run method.


The code above also disables the button. It will be re-enabled when the threaded task exits.


Let's write the code that does some work now. Method RunHelloWorld will be executed in the background thread.

procedure TfrmTestOTL.RunHelloWorld(task: IOmniTask);
begin
task.Comm.Send(0, 'Hello, world!');
end;

The OTL automatically sets up two-way channel between task control interface (the one that is returned from the CreateTask) and the threaded task (the IOmniTask interface passed to the worker method). Both expose a property named Comm that provides this communication. At this time, the communication channel is very simplistic - you can only send (word, Variant) pairs (message ID and data). In this case, I'm setting message ID to 0 and message data to 'Hello, world!'.


That is everything that has to be done on the threaded side. On the GUI side, the OmniTaskEventDispatch1 component is already set to monitor messages from the threaded task. We only have to write an OnTaskMessage handler.

procedure TfrmTestOTL.OmniTaskEventDispatch1TaskMessage(task: IOmniTaskControl);
var
msgID : word;
msgData: TOmniValue;
begin
task.Comm.Receive(msgID, msgData);
lbLog.ItemIndex := lbLog.Items.Add(Format('[%d/%s] %d|%s', [task.UniqueID, task.Name, msgID, msgData]));
end;

The OnTaskMessage handler is called each time the message is available. The code reads this message and shows it in the listbox. As you can also see, each task is assigned an unique ID.


To enable the button when task is done, you have to write an OnTaskTerminated event handler.

procedure TfrmTestOTL.OmniTaskEventDispatch1TaskTerminated(task: IOmniTaskControl);
begin
lbLog.ItemIndex := lbLog.Items.Add(Format('[%d/%s] Terminated', [task.UniqueID, task.Name]));
btnHello.Enabled := true;
end;

The code doesn't check which task has completed as we can only have one task running.


Run it and press the button few times. You should see that the threaded task gets allocated new unique ID every time it is run.


image


This example is available in the OTL repository in folder tests\3_HelloWorld_with_package.


If you don't want to install the package, you can create TOmniTaskEventDispatch component on the fly. An example using this approach is available in the repository in folder tests\1_HelloWorld.

Why is software third time lucky?

In the beginning, there is a Problem.

Customer knows that the Problem exists, but can't tell exactly what the Problem is.

So the Customer goes to to a Programmer and asks him [or her, of course, but let me keep this simple]: "Can you build me a program that does That?"

[Here lies the original sin. Customer never asks: "Can you help me solve the Problem?" Because of that, Programmer is writing a program to do That instead of program that does This.]

Programmer says: "I can," because we programmers, by our nature, want to believe that we can code anything. So the Programmer goes to work and he works and works and although he has no idea what he is working on and how it should be built, he makes the Version One. It is merely a research project, a tool that allows the Programmer to understand the problem (not the Problem, just the problem of coding a program that does That), but the pressure of the modern world forces him to deliver it to the Customer.

Of course, Version One is unusable. After all, it was merely a sandbox where the Programmer tried to understand the problem. So the Customer pretends that he is using it and from time to time orders a small change, just to make the Programmer think that his software is being used.

After some time, Customer starts to think that he should really get something useful for his money, not just the unusable Version One and thus commissions the Programmer to write a new, improved version. Mind you, the Customer still hasn't a faintest clues what the Problem is, and so doesn't the Programmer. Even more, Programmer still has no idea that he has to solve the Problem. His task is to write a program that does That.

Still, something has improved since the day 1 - Programmer's understanding on how to write a program that does That. So he goes and codes much better program, which is built on his previous experiences. And thus the Version Two is born.

Version Two is much better version. It actually works. It doesn't look like a five year old has put it together in one afternoon and it doesn't crash every five minutes. And so, the Customer starts using it. And after some time, he sadly recognizes that the Programmer has indeed written a wonderful software that does That, but that this, alas, doesn't help him solve the Problem at all.

But now the Customer can finally tell the Programmer what the Version Two should do differently. And the Programmer listens and finally says in awe: "You mean that it should do This instead of That?!? Why didn't you say that in first place?"

And thus, the Version Three is born. It is written well. It solves the Problem. The Customer is happy. And the Programmer lives on to fight yet another battle.

This I believe.

Monday, June 30, 2008

OmniThreadLibrary Example #1: Beep, world!

image In my previous post, I promised to publish some examples of OTL usage. Here is the first one - a simplification of the Hello, World program. This example doesn't write anything to the screen, it just makes a noise.

Start by creating a new Delphi VCL application. Drop a button on the form and write OnClick handler.

procedure TfrmTestOTL.btnBeepClick(Sender: TObject);
begin
CreateTask(Beep, 'Beep').Run;
end;

This will create a threaded task with name 'Beep' and main method Beep. Then the code will create a new thread and start executing this task in the context of the newly created thread. That's all, folks!


To make the example fully functional, add OtlTask unit to the uses list and write the Beep method.

procedure TfrmTestOTL.Beep(task: IOmniTask);
begin
MessageBeep(MB_ICONEXCLAMATION);
end;

Run the program, click the button. It will beep!


If you don't believe that the code is executed in a secondary thread, place a breakpoint on the MessageBeep call and check the Thread Status window. It will clearly indicate that the breakpoint was triggered from a secondary thread.


image


This example is included in the OTL repository in folder tests/0_Beep.

OmniThreadLibrary - threading library for Delphi [WIP]

imageIn my $$$ line of programming, everything revolves around threads. Servers, multithreaded processing on clients, real time hardware control ... you name it. In all the years I've learned many dos and don'ts of multithreaded programming. I've also written few frameworks to simplify this job and most of them are already retired. Sometimes a potentially good approach turns out not to be so good at all ...

In the last year I've become more and more unhappy with my current framework and decided that it's time for something better, something simpler. Something built from scratch. This time I didn't start with the implementation but tried to visualize how I'd like to use my framework in real-life applications. After all, I had all those test cases lying around in form of my old applications using old framework. And so, the OmniThreadLibrary was born.

Currently OTL is in a very fluent alpha state. I'm already using it in my projects but some more important interfaces are changing from day to day. Still, you're invited to try it and to comment on its usefulness. If you can give me an example of real-world problem that can't be implemented using OTL, all the better. Maybe I'll be able to enhance the OTL to suite your purpose more.

OTL is living on the Google Code hosting and is BSD licensed. Current implementation only supports Delphi 2007 (although I'd been told that only small modifications are required to make it work with Delphi 2006) and unless I'm given a very convicting business case support for older Delphis won't be included. Let's look into future, not into past.

Some examples are included in the repository, but not all parts of the current infrastructure are explored. I'll post more on OTL and describe examples and OTL's inner workings on this blog in the next few days.

Monday, June 23, 2008

TDM Rerun #9: My Data Is Your Data

There is no multiprocess solution without data sharing. If you have to do something useful and need more than one application to do it, you need a way to share data between those applications.

- My Data Is Your Data, The Delphi Magazine 88, December 2002

In TDM #88 my synchronisation & sharing theme continued with an article on shared memory. The article first demonstrated how shared memory can be implemented with file mapping Win32 API and then proceeded with the description of a wrapper that simplified this operation. This wrapper also added some useful extensions, as are text, stream and array access to the shared memory.

GpSharedMemory is still alive and well and used in many projects. Since the 2002 article it gained resizable shared memory, shared stream support and shared linked list.

Links: article (PDF, 60 KB), source code (ZIP, 50 KB), current GpSharedMemory unit

Sunday, May 11, 2008

Delphi compatibility restored

A recent update to my GpLists unit broke compatibility with all Delphi versions that don't have enumerator support. Sorry :(

This has now been fixed.

Thursday, May 01, 2008

TDM Rerun #8: A Synchronisation Toolkit

Every programmer accumulates reusable code over the years and I am no exception. Sometimes, parts of that code seem generic enough to be worth documenting and presenting it to the public.

Today, I'd like to put on display four classes belonging to the inter-process synchronisation part of my personal toolbox.

- A Synchronisation Toolkit, The Delphi Magazine 86, October 2002

In October 2002 I presented my GpSync unit, which is still very actively updated and developed. At the time of writing, there were four reusable classes in this unit - a token, a group, a counted group and single-write-multiple-readers reimplementation copied (and translated from C to Delphi) from Advanced Windows by Jeffrey Richter.

Since that first public occurrence, GpSync was extended with a flag, a counter, a message queue (shared-memory based, can be used to send messages from an interactive application to a service and back), and a circular buffer. To this day, it represents most common building blocks I'm using in multithreaded applications.

 

Links: article (PDF, 148 KB), source code (ZIP, 196 KB), current GpSync unit

Monday, April 21, 2008

Enumerating XML nodes

I just submitted an update to OmniXmlUtils.pas - a helper library for the Delphi implementation of the XML DOM model, the OmniXML. It allows you to walk over child nodes with a simple enumerator (but you already saw that coming, huh?).

In OmniXML (and in MS XML on which the OmniXML interface is based), you access child nodes via IXMLNodeList interface. Actually, when you access any subset of nodes (for example, when you call SelectNodes and pass it an XPath expression), you are using IXMLNodeList. More and more I was using it, more I hated its stupid interface.

  IXMLCustomList = interface
function GetLength: Integer;
function GetItem(const Index: Integer): IXMLNode;
property Item[const Index: Integer]: IXMLNode read GetItem;
property Length: Integer read GetLength;
end;

IXMLNodeList = interface(IXMLCustomList)
procedure Reset;
function NextNode: IXMLNode;
end;
[I removed declarations of all functions that are not used when walking over the list.]


Basically, I have two options with the current design. I can use a standard for loop using Length and Item[] or I can use NextNode and while loop.


Or, of course, I can write an enumerator - and that's what I did. XMLEnumNodes takes an IXMLNodeList and wraps it in an enumerator. Even better - it takes an xml node or document and an XPath expression and runs SelectNodes automatically for me so I can write code like this:

for nodePassword in XMLEnumNodes(xmlConfig, '//*/PasswordHash') do
SetTextChild(nodePassword, '(removed)');

Now that's what I call a nice code!


If you can't wait for tomorrow update to the OmniXML daily snapshot, you can download OmniXMLUtils 1.25 here.

Tuesday, April 01, 2008

TDM Rerun #7: File Sharing on Linux

In the Windows world we are used to file locking. It is something natural, something that permeates the operating system, something that we had to learn when we stopped using DOS. But for all practical purposes, Linux is Unix, and Unix was designed with different things in mind, one of which was openness and sharing. From the very old days, Unix has supported different kinds of file systems, including those that span a network. Remember, that was well before Windows, and file locking was something that was simply too expensive (in terms of time and network traffic) to be implemented. And that is why there is no locking on Linux.

- File Sharing on Linux, The Delphi Magazine 84, August 2002

The year was 2002, Kylix was still alive and I was planning to write some software for the new platform. Sadly, that never happened - other (Windows) projects were more important and before I came back to Kylix, it was more or less dead.

GpLinuxFileSync testerStill, this short excursion was responsible for my only Linux article. I took my  GpFileSync unit from TDB 68: Let's Cooperate and tried to adapt it to Linux. As it turned out, this was nearly an impossible mission. The ideas behind Unix file systems almost totally prevent its abuse in such direction. To make myself clear - it is certainly possible to use file system for locking, but there are big problems where you want to a) make the lock disappear if the program terminates abnormally, b) put the lock file on a networked location (NFS) or c) all of the above.

I ended with three less than perfect solutions. First used a simple lock file, second was an adaptation of the first that worked with the NFS 2 volums (both were not able to auto-delete the file-mutex on program termination) and third used fcntl() call (auto-delete worked well, but NFS was not fully supported). As far as I know, none of them was ever tested in a real-world application.

Links: article (PDF, 125 KB), source code (ZIP, 16 KB), current GpFileSync unit