It is All about Traffic, and Programming
Search
Archives
0 visitors online now
0 guests, 0 members

Delphi

Who touches my “for-loop” condition?

A for loop is a common control structure that allows repetitions of certain statements for a specific number of times. The syntax of a for-loop in C++ or C# is

for (initialization; condition; increment) {
   statement(s);
}

As simple as ABC. As easy as a no-brainer. As happy as Clam.

Now, look at the following C++ code. How many times would the the loop execute?

int main( )
{
   int N = 10;

   for ( int i = 0; i < N; i++ ) {
     N--;
     std::cout << "i = " << i 
               << ";"
               << "N= "  << N 
               << std::endl;
   }

   return 0;
} 

As the output shown below – The loop executes 5 times, even though we set N = 10.  Obviously, modifying N inside the loop body, i.e., N–, causes the termination of the loop prematurely.

Generally speaking, it is a bad practice to modify loop termination condition inside a for-loop. Bad, Bad, Bad!

Now, the same question for a similar Delphi code:

uses
  System.SysUtils;

var
  I, N: Integer;

begin
  try
    N := 10;

    for I := 0 to N do
    begin
      Dec(N);
      WriteLn( Format('I=%d, N=%d',[I, N]) );
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

The output is not so much unsurprising – unlike the C++ counterpart, it does loop 11 times:

Interesting. It seems modifying N inside loop body doesn’t impact the number of intended loop execution times.

Let’s check the disassembled C++ binary:

As seen from the highlighted lines, N is referenced by address (i.e., ptr [N]) in loop body. This means, any change to N’s value will eventually impact the termination condition evaluation. Address [ebp-14h] is the local variable i stored in Stack, which is incremented with every repetition of the loop, before comparing to ptr[N] that is decremented with each loop.

Delphi compiler generates something different:

N is addressed at [0x004258a8]. At the beginning of the loop, N’s value is copied to [$ebp-$14]. Each loop just checks [$ebp-$14] for termination. Modifying the original Nwouldn’t impact the loop termination condition evaluation.

So, though it is generally not a good practice to change for-loop termination condition inside the loop,  Delphi obviously has some compiler caveat that may slip the mind of a careless programmer. Be warned!

Advanced Aimsun API Programming (2)

Aimsun MicroAPI has a total of 444 functions,  in three header files:

  • AKIProxie
  • ANGConProxie
  • CIProxie

These functions are ported to Delphi language, and the following shows a demo of obtaining vehicle information:

Aimsun API-Delphi Demo 1: Obtaining Vehicle Information

  • First the exported functions are defined in the project file, as follows:
// AIMSUN API  Delphi Interface
// Ported by Wuping Xin
// Last Update @2014-03-21 20:23:55
//---------------------------------------------------

library AAPI_veh_info;

uses
  AAPI in 'AAPI.pas',
  AAPI_Util in 'AAPI_Util.pas',
  AKIProxie in 'AKIProxie.pas',
  ANGConProxie in 'ANGConProxie.pas',
  CIProxie in 'CIProxie.pas';

{$R *.res}

exports
  AAPILoad,
  AAPIInit,
  AAPIManage,
  AAPIPostManage,
  AAPIFinish,
  AAPIUnLoad,
  AAPIEnterVehicle,
  AAPIExitVehicle,
  AAPIEnterVehicleSection,
  AAPIExitVehicleSection,
  AAPIInternalName,
  AAPIPreRouteChoiceCalculation;

begin
  //
end.
  • Second,  the exported functions are actually implemented in unit AAPI.pas file, as follows:
unit AAPI;

interface
  function AAPILoad: Integer; cdecl;
  function AAPIInit: Integer; cdecl;
  function AAPIManage(aTime: Double; aTimeSta: Double; aTimeTrans: Double;
      aSimStep: Double): Integer; cdecl;
  function AAPIPostManage(aTime: Double; aTimeSta: Double; aTimeTrans: Double;
      aSimStep: Double): Integer; cdecl;
  function AAPIFinish: Integer; cdecl;
  function AAPIUnLoad: Integer; cdecl;

  function AAPIEnterVehicle(aVehID: Integer; aSectionID: Integer): Integer; cdecl;
  function AAPIExitVehicle(aVehID: Integer; aSectionID: Integer): Integer; cdecl;
  function AAPIEnterVehicleSection(aVehID: Integer; aSectionID: Integer; aTime:
      Double): Integer; cdecl;
  function AAPIExitVehicleSection(aVehID: Integer; aSectionID: Integer; aTime:
      Double): Integer; cdecl;
  function AAPIInternalName: PAnsiChar; cdecl;

  function AAPIPreRouteChoiceCalculation(aTime: Double; aTimeSta: Double):
      Integer; cdecl;

implementation

uses
  SysUtils, AKIProxie, ANGConProxie, CIProxie;

  function AAPIEnterVehicle(aVehID: Integer; aSectionID: Integer): Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIEnterVehicleSection(aVehID: Integer; aSectionID: Integer; aTime:
      Double): Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIExitVehicle(aVehID: Integer; aSectionID: Integer): Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIExitVehicleSection(aVehID: Integer; aSectionID: Integer; aTime:
      Double): Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIFinish: Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIInit: Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIInternalName: PAnsiChar; cdecl;
  begin
    Result := 'AAPI_veh_info';
  end;

  function AAPILoad: Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIManage(aTime: Double; aTimeSta: Double; aTimeTrans: Double;
      aSimStep: Double): Integer; cdecl;
  var
    lvVehInfo: InfVeh;
    lvNumOfSections, lvNumOfVehs, lvNumOfJunctions: Integer;
    lvSectionIdx, lvVehIdx, lvJunctionIdx: Integer;
    lvSectionID, lvJunctionID: Integer;
    lvVehInfoStr: string;
  begin
    try
      lvNumOfSections := AKIInfNetNbSectionsANG;

      for lvSectionIdx := 0 to lvNumOfSections - 1 do
      begin
        lvSectionID := AKIInfNetGetSectionANGId(lvSectionIdx);
        lvNumOfVehs := AKIVehStateGetNbVehiclesSection(lvSectionID, True);

        for lvVehIdx := 0 to lvNumOfVehs - 1 do
        begin
          lvVehInfo := AKIVehStateGetVehicleInfSection(lvSectionID, lvVehIdx);
          lvVehInfoStr := Format('Vehicle %d, Section %d, Lane %d, CurrentPos %f, CurrentSpeed %f',
            [
              lvVehInfo.idVeh,
              lvVehInfo.idSection,
              lvVehInfo.numberLane,
              lvVehInfo.CurrentPos,
              lvVehInfo.CurrentSpeed
            ]);

          AKIPrintString(PAnsiChar(AnsiString(lvVehInfoStr)));
        end;
      end;

      lvNumOfJunctions := AKIInfNetNbJunctions;

      for lvJunctionIdx := 0 to lvNumOfJunctions - 1 do
      begin
        lvJunctionID := AKIInfNetGetJunctionId(lvJunctionIdx);
        lvNumOfVehs := AKIVehStateGetNbVehiclesJunction(lvJunctionID);

        for lvVehIdx := 0 to lvNumOfVehs - 1 do
        begin
          lvVehInfo := AKIVehStateGetVehicleInfJunction(lvJunctionID, lvVehIdx);
          lvVehInfoStr := Format('Vehicle %d, Node %d, From %d, To %f, CurrentPos %f, CurrentSpeed %f',
            [
              lvVehInfo.idVeh,
              lvVehInfo.idJunction,
              lvVehInfo.idsectionFrom,
              lvVehInfo.idSectionTo,
              lvVehInfo.CurrentPos,
              lvVehInfo.CurrentSpeed
            ]);

          AKIPrintString(PAnsiChar(AnsiString(lvVehInfoStr)));
        end;


      end;

      Result := 0;
    finally
      //
    end;
  end;

  function AAPIPostManage(aTime: Double; aTimeSta: Double; aTimeTrans: Double;
      aSimStep: Double): Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIPreRouteChoiceCalculation(aTime: Double; aTimeSta: Double):
      Integer; cdecl;
  begin
    Result := 0;
  end;

  function AAPIUnLoad: Integer; cdecl;
  begin
    Result := 0;
  end;

end.

 The above demo and the ported Aimsun API in Object Pascal can be downloaded from here.

http://wupingxin.net/wx-files/aimsun_api_pascal.zip

 

Delphi exception, Fortran mystery

A mysterious exception,  which  after I scratched my head for quite a while,  turned out to be a simple overlook.

In a recent testing of a new internal beta release of some software , we came across a fatal exception thrown from the main program by the end of the simulation run. It happened when the program was trying to export the data to a network SQL server.

What was perplexing is that the exception only happened on testing machines;  on the developer’s machine,  everything seemed normal.

After pulling some of my hairs, it turned out –

On the development machine,  we have the capacity-limited SQL Server Express 2008 R2 installed for debugging and testing purpose.  When developing,  this  server is restricted as a local server and set as the default database connection. The system has a separate ini file that  overwrites the default setting and specifies the real database server to connect.

The SQL server connection instance, when created and initialized with its ctor,  uses the default connection set up, which will try to create a connection in the first place to the local SQL server on the development machine. Something like this:

[delphi]

lvSQLConn := TMSSQLConnection.Create;

try
try

lvSQLConn.ServerAddress := (server address specified from the ini file)
except

end;
finally
lvSQLConn.Free;
end;
[/delphi]

Therefore the default connection happens during the construction.  Of course,  when running on  the development machine,  there will be absolutely no problem,  because that local SQL server will always be available to access as Named Pipe.   However, on the deployment machine,  that local SQL server becomes inaccessible, hence an exception (something like “Server doesn’t support requested protocol“) is thrown right in the middle of construction, which as you  can see from the code, is not caught by the try-except-finally, but caught by its host, which has no idea about the type of exception.

The fix is very simple and straightforward:

  •   Move the class construction line into the try-except-finally;
  •   Turn off the default connection (i.e., set the Active := False for the data access component during design time).

The morale of this story?

  • Be wary of (or try not to do) resource allocation during an object construction;
  • Be careful with class constructor so exceptions would not be thrown, or if thrown, get properly handled.

Note on OmniThreadLibrary (OTL): Communication Channel

To facilitate communication between two thread task workers,  OmniThreadLibrary provides a very handy mechanism, i.e., communication channel.   Two different task works, each can register the one end point of the channel, and communicate through that channel like this:

Taken from demo 8 RegisterComm,

In the main thread, create the communication channel, the task controls, and associate the two end points of the channel with each of the task worker:


procedure TfrmTestRegisterComm.FormCreate(Sender: TObject);
begin
  FCommChannel := CreateTwoWayChannel(1024);
  FClient1              := CreateTask(TCommTester.Create(FCommChannel.Endpoint1))
                                  .MonitorWith(OmniTED)
                                  .Run;
  FClient2              := CreateTask(TCommTester.Create(FCommChannel.Endpoint2))
                                  .MonitorWith(OmniTED)
                                  .Run;
end;

In the task worker’s constructor, keep a local reference to the end point:

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

In the task worker’s initializer, register the end point with its task:

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

From the above, you can see, the steps are:

  1. Create the two way channel using “CreateTwoWayChannel”,  the number 1024 indicates the length of the message queue;
  2. Associate the two end points of the channel with two task workers;
  3. Inside the task worker’s Initialize( ),  register the communication end point with the worker’s IOmniTask.

After all these are done, and wen a task worker wants to talk to the other task worker, simply do something like below, then the other task worker’s MSG_FORWARDING message handler will be invoked.  Isn’t that nice?

Therefore, if a task worker wants to talk to its task control,  it can keep using Task.Comm.Send, or if it wants to talk to another task worker, use
ctComm.Send(MSG_FORWARDING,  msg.MsgData);

In case you want a task worker to send messages to ITSELF,   you can register the same communication channel “twice” with the same task worker like the following:


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

Cool!

Using Delphi DLL with C++

Delphi has  good support for database applications.  Recently I have been  working on a scientific application in C++/Qt – the data access part of this application was  implemented as a Delphi DLL .

In order to use a Delphi DLL with a C++ host, there are a few options:

  1. Generate proper import lib  by defining your own def file, see this post
  2. Convert Delphi DLL to static lib using this tool
  3. Or dynamically loading the DLL at run time.  For that,  you’ll  have to do the following tedious steps(excerpted from MSDN):
// Define the function prototypes
 typedef short (CALLBACK* FindArtistType)(LPCTSTR);
// Load the DLL using the LoadLibrary function, and keep 
// the handle to the DLL instance.  
dllHandle = LoadLibrary("art.dll");
// Get a pointer to each function using the GetProcAddress 
// function. Cast function pointers to the types defined in the
// first step. 
FindArtistPtr = (FindArtistType)GetProcAddress(dllHandle,
"FindArtist");
// Verify function pointers, and use the fuctions
if (runTimeLinkSuccess = (NULL != FindArtistPtr))
   short retVal = FindArtistPtr(myArtist);
// Unload the DLL.
freeResult = FreeLibrary(dllHandle);
Qt provides a very nice class, QLibrary to streamline the above procedures, and the nice thing is, the instance is ref counted, hence the DLL won’t be unloaded unless all QLibrary instances go out of scope. The following is the sample code that loads the dll  developed in Delphi. Isn’t that nice?

Note on OmniThreadLibrary (OTL): ChainTo/MsgWait

  • ChainTo is a very handy decorator of IOmniTaskControl interface – the use case is

[we have a list of tasks, and we want to assign these tasks to a list working threads, one task a thread, and have these threads executing one after another, sequentially]

For example (Sample 16),

procedure TfrmTestChainTo.btnStartTasksClick(Sender: TObject);

var

task1: IOmniTaskControl;

task2: IOmniTaskControl;

task3: IOmniTaskControl;

taskA: IOmniTaskControl;

begin

task3 := CreateTask(BgTask, ‘3’).MonitorWith(OmniTED);

task2 := CreateTask(BgTask, ‘2’).MonitorWith(OmniTED).ChainTo(task3);

task1 := CreateTask(BgTask, ‘1’).MonitorWith(OmniTED).ChainTo(task2);

task1.Run;

taskA := CreateTask(BgTask, ‘A’).MonitorWith(OmniTED).ChainTo(

CreateTask(BgTask, ‘B’).MonitorWith(OmniTED).ChainTo(

CreateTask(BgTask, ‘C’).MonitorWith(OmniTED)));

taskA.Run;

end;

When task1.Run called, task1 runs first, after it finishes, task2 starts then task 3.

  • MsgWait

MsgWait is needed if the worker thread calls something that involves windows message loop. For example, a timer .

FHelloWorld := CreateTask(THelloWorld.Create(), ‘Hello, World!’)

.MonitorWith(oeMonitor)

.SetTimer(500, MSG_INTERNAL_TIMER)

//.MsgWait

.Run;

MsgWait is not needed for SetTimer, but needed for the following:

function THelloWorld.Initialize: boolean;

begin

FTimer := TDSiTimer.Create(true, 1000, DoTimer);

Result := true;

end;

The following is exercpted from The Delphi Geek Blog:

[http://www.thedelphigeek.com/2008/09/processing-windows-messages-in.html

MsgWait

In case you want to learn more about OTL internals, read ahead …

Let’s take a short look at the .MsgWait implementation. The function itself just sets two internal fields and returns the object itself so that we can chain another method to it.

function TOmniTaskControl.MsgWait(wakeMask: DWORD): IOmniTaskControl;

begin

 Options := Options + [tcoMessageWait];

 otcExecutor.WakeMask := wakeMask;

 Result := Self;

end; { TOmniTaskControl.MsgWait }

The hard work is done in TOmniTaskExecutor.Asy_DispatchMessages. If the tcoMessageWait option is set, the MsgWaitForMultipleObjectsEx will also wait for Windows messages (in addition to everything else it does) because it will receive non-null waitWakeMask. When a message is detected, the code will call ProcessThreadMessages method which simply peeks and dispatches all Windows messages (and Delphi’s internal message dispatch mechanism takes care of all the rest).

if tcoMessageWait in Options then

 waitWakeMask := WakeMask

else

 waitWakeMask := 0;

//...

awaited := MsgWaitForMultipleObjectsEx(numWaitHandles, waitHandles,

 cardinal(timeout_ms), waitWakeMask, flags);

//...

else if awaited = (WAIT_OBJECT_0 + numWaitHandles) then //message

 ProcessThreadMessages


procedure TOmniTaskExecutor.ProcessThreadMessages;

var

 msg: TMsg;

begin

 while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) and (Msg.Message <> WM_QUIT) do begin

   TranslateMessage(Msg);

   DispatchMessage(Msg);

 end;

end; { TOmniTaskControl.ProcessThreadMessages }

The Asy_DispatchMessages is probably the most complicated part of the OTL (once you understand the lock-free structures inside the OtlContainers 😉 Once you understand how it works you’ll be fully prepared to write custom thread loops in highly specialized threaded code. But don’t worry, you can use OTL even if you don’t understand the magic hidden inside.

Note: The above is from the Delphi Geek Blog by Primoz Gabrijelcic, the Delphi Guru and developer of the great OmniThreadLibrary.

]

Note on OmniThreadLibrary (OTL): Task Group

As we have noted in a previous post that a task group can be used to “group” control a group of thread tasks.

The interface is declared as follows:

IOmniTaskGroup = interface [‘{B36C08B4-0F71-422C-8613-63C4D04676B7}’]

function GetTasks: IOmniTaskControlList;

function Add(const taskControl: IOmniTaskControl): IOmniTaskGroup;

function GetEnumerator: IOmniTaskControlListEnumerator;

function RegisterAllCommWith(const task: IOmniTask): IOmniTaskGroup;

function Remove(const taskControl: IOmniTaskControl): IOmniTaskGroup;

function RunAll: IOmniTaskGroup;

procedure SendToAll(const msg: TOmniMessage);

function TerminateAll(maxWait_ms: cardinal = INFINITE): boolean;

function UnregisterAllCommFrom(const task: IOmniTask): IOmniTaskGroup;

function WaitForAll(maxWait_ms: cardinal = INFINITE): boolean;

property Tasks: IOmniTaskControlList read GetTasks;

end; { IOmniTaskGroup }

The most interesting interesting interface functions are listed below –

function RunAll: IOmniTaskGroup;

procedure SendToAll(const msg: TOmniMessage);

function TerminateAll(maxWait_ms: cardinal = INFINITE): boolean;

For example (sample 15)

procedure TfrmTestTaskGroup.btnStartTasksClick(Sender: TObject);

var

i: integer;

begin

FTaskGroup := CreateTaskGroup;

for i := 1 to 10 do

CreateTask(TMyWorker.Create()).MonitorWith(OmniTED).Join(FTaskGroup);

Log(‘Starting all tasks’);

FTaskGroup.RunAll;

end;

and

procedure TfrmTestTaskGroup.btnStopTasksClick(Sender: TObject);

begin

if assigned(FTaskGroup) then begin

FTaskGroup.TerminateAll;

FTaskGroup := nil;

Log(‘All stopped’);

end

else

Log(‘Nothing to stop’);

end;

Note on OmniThreadLibrary (OTL): TerminateWhen

Once a worker thread has been launched, there are a few ways to terminate it:

  • IOmniTaskControl.Terminate

This will set the termination signal, and if it is parameter-less, then the worker will wait indefinitely till the thread finishes normally. If a wait-out time is specified, the work will wait until the wait-out time, then kill the thread forcefully, if it has not finished.

If a thread is killed forcefully, there might be resource leak (Cleanup and destructor may not get invoked).

  • IOmniTaskControl.TerminateWhen

Use an event, or an IOmniCancellationToken, for example (sample 14),

procedure TfrmTestTerminateWhen.btnStartTasksClick(Sender: TObject);
var
i: integer;
begin
if assigned(FCounter) and (FCounter.Value > 0) then
btnStopTasksClick(Sender);
FCounter := CreateCounter(10);
FTerminate := CreateOmniCancellationToken;
for i := 1 to FCounter.Value do begin
Log(Format(‘Task started: %d’,
[CreateTask(TMyWorker.Create()).TerminateWhen(FTerminate).WithCounter(FCounter).
MonitorWith(OmniTED).Run.UniqueID]));
end;

end

Then, somewhere in the code, call

FTerminate.Signal

This is equivalent to calling IOmniTaskControl.Terminate, i.e., it will set the termination signal and wait indefinitely till the thread finishes.  When the termination signal is set, IOmniTask.Terminated will be True

If the thread is currently inside an intense looping, we can use the following methods to jump out of the looping immediately

[method1]
while True do
begin

if WaitForSingleObject(task.TerminateEvent, 0) = WAIT_OBJECT_0 then
Break;

end

or
[method2]
while True do
begin

if task.Terminated then
Break;

end

or

[method3]
while True do
begin

if task.CancellationToken.IsSignalled then
Break;

end

To use the last one, we need to associated a cancellation token as follows:

CreateTask(TMyWorker.Create())
.TerminateWhen(FTerminate)
.CancelWith(FTerminate)
.WithCounter(FCounter)
.M
onitorWith(OmniTED)
.Run

Note on OmniThreadLibrary (OTL): Exception

When an exception happens from a working thread, OTL allows to pass the exception information to the calling thread (i.e., the one where the relevant IOmniTaskControl is created). (See OTL sample 13)

The information is saved in IOmniTaskControl.FatalException, it is a normal Exception sub class, therefore if it is not nil, then from its class name and message, one can get the exception information. For example,

if Assigned(aTaskControl.FatalException) then

lvExInfo := aTaskControl.FatalException.ClassName + ‘: ‘ + aTaskControl.FatalException.Message;

In sample 13, the decorator Enforced(True) is used, i.e.,

CreateTask(TExceptionTest.Create(Sender = btnInitException)).Enforced(True)

According to the author –

“…In short – OTL always tries to execute your task. If you call taskControl.Terminate before the task has even started, OTL will set the termination signal and start executing task. This is not a good idea if the task was waiting in the thread pool queue and threadPool.CancelAll or threadPool.Terminate was executed. To bypass this auto-execute behaviour, you can call .Enforced(false).

Given the above statement, Enforced(True) is not required, since by default it is already set to true. Therefore, sample 13 runs the same if we remove Enforced(True).

Some special remarks for exceptions that happen with TOmniWorker instances –

  • When an exception occurs, be it from inside a worker object’s Initialize( ), Cleanup( ), or an event handler for an internal timer, the worker thread will be immediately terminated (by its task control); it is after the termination, the exception information is assigned to its task control’s FatalException property, and available from there.

In all, I am not quite sure about the rationale of this behavior, but I guess the author’s intention is to pass the exception information to the task control in the first place, though, before that, some necessary handling must have been performed (at the user’s responsibility) in the worker thread where the exception is raised before passing that information. Otherwise, I would expect high possibility of resource leak since after the exception the work thread will be terminated unconditionally and depending on where the exception happens, Cleanup( ), and/or the worker object’s destructor may not even get invoked.

Note on OmniThreadLibrary (OTL): Lock

These series posts on OmniThreadLibrary (OTL) only serve as my personal mnemonics notes on the usage of OTL. OTL’s internal workings are quite sophisticated, as documented in several scattering posts by the original author http://otl.17slon.com/tutorials.htm

For thread synchronization, OTL implements Spin Lock, and Ticket Spin Lock. More information on Spin Lock from here, and Ticket Spin Lock from here.

Under OTL framework, a lock can be associated with a IOmniTaskControl by

function WithLock(const lock: TSynchroObject; autoDestroyLock: boolean = true): IOmniTaskControl; overload;

function WithLock(const lock: IOmniCriticalSection): IOmniTaskControl; overload;

For example (excerpted from OTL sample 11)

procedure TfrmTestLock.btnTestLockClick(Sender: TObject);

var

task : IOmniTaskControl;

iRepeat: integer;

begin

task := CreateTask(TestArray);

if sender = btnTestLock then

task.WithLock(TTicketSpinLock.Create);

task.Run;

for iRepeat := 1 to CTestRepetitions do

if not OneTest(task.Lock) then

begin

Log(Format(‘Test failed at repetition %d’, [iRepeat]));

task.Terminate;

break; //for iRepeat

end;

task.WaitFor(INFINITE);

Log(‘Completed’);

end;

The autoDestroyLock by default is true, meaning the reference count will be decremented when the omni task control goes out of scope.