Thursday, April 11, 2013

ThreadSafe Lock Manager: [3] Test

After you have designed and written some code, you should always test it. This is especially important for the multithreaded code which rarely works on the first (or the second, or third) try.

To test the lock manager, I have written a test 54_LockManager which can be found in the OmniThreadLibrary SVN.

Bad Code

A good approach to writing multithreaded tests is to start with a code that is proven not to work. When you have a failing test, add the necessary corrections (in our case, introduce the lock manager) and see if the code starts working.

My test starts multiple tasks, each repeatedly doing the following subtask:

  • Select a random slot in an array.
  • Increment the value in the slot.
  • Release the thread slice (to make the unsafe data access more obvious).
  • Decrement the value in the slot.

At the end, all values are printed out. Ideally, they should all be zero, but as the code is buggy, the result will be unpredictable.

procedure TfrmTestLockManager.btnUnsafeClick(Sender: TObject);
var
cnt:
TGp4AlignedInt;
begin
Prepare;
cnt.Value := 0
;
Parallel.ParallelTask.NumTasks((Sender as
TButton).Tag).Execute(
procedure
var
slot:
integer;
startTime:
int64;
begin
startTime :=
DSiTimeGetTime64;
while not DSiHasElapsed64(startTime, CTestDuration_sec*1000) do
begin
slot := Random(CHighSlot) + 1
;
FValues[slot] := FValues[slot] + 1
;
DSiYield;
FValues[slot] := FValues[slot] - 1
;
cnt.Increment;
end
;
end
);
ShowValues(cnt.Value, (Sender as
TButton).Tag);
end
;

Good Code


Corrected version uses TOmniLockManager<integer> to lock access to the slot the thread is working on. In the code below, important changes are marked yellow.

procedure TfrmTestLockManager.btnSafeClick(Sender: TObject);
var
cnt :
TGp4AlignedInt;
lockManager:
TOmniLockManager<integer>;
begin
Prepare;
cnt.Value := 0
;
lockManager :=
TOmniLockManager<integer>.Create(CHighSlot);
try
Parallel.ParallelTask.NumTasks((Sender as
TButton).Tag).Execute(
procedure
var
slot:
integer;
startTime:
int64;
begin
startTime :=
DSiTimeGetTime64;
while not DSiHasElapsed64(startTime, CTestDuration_sec*1000) do
begin
slot := Random(CHighSlot) + 1
;
if lockManager.Lock(slot, CTestDuration_sec*1000) then
try
FValues[slot] := FValues[slot] + 1
;
DSiYield;
FValues[slot] := FValues[slot] - 1
;
cnt.Increment;
finally lockManager.Unlock(slot); end
;
end
;
end
);
finally FreeAndNil(lockManager); end
;
ShowValues(cnt.Value, (Sender as
TButton).Tag);
end
;

Speed


I compared the “corrected” code with another implementation using Delphi’s TMonitor. To measure the speed, I have counted the total number of Increment/Decrement operations done in one millisecond (measured as an average over ten seconds). I have expected TMonitor to perform better than my code because it is basically just a wrapper around a critical section, but I certainly didn’t expect it to perform as much better as the results are showing.


TOmniLockManager on 24 threads

221 operations/ms

TMonitor on 24 threads

3839 operations/ms

Clearly, using critical sections or TMonitor is much better solution if you can implement it in your algorithm. For my problem, where I expect to process less than 10 requests per second, the TOmniLockManager is a good enough solution. Still, in the future I may run it through a profiler to find the bottleneck.

No comments:

Post a Comment