I was showing off the new Parallel Programming Library (PPL) at a recent event, in particular the demo where we spawn off a couple of tasks and then wait for only one of them to finish. The code in question looked like this:
procedure TFormThreading.Button2Click(Sender: TObject);
var
tasks: array of ITask;
value: Integer;
begin
Setlength (tasks ,2);
value := 0;
tasks[0] := TTask.Create (procedure
begin
sleep (3000);
TInterlocked.Add(value, 3000);
end);
tasks[0].Start;
tasks[1] := TTask.Create (procedure
begin
sleep (5000);
TInterlocked.Add (value, 5000);
end);
tasks[1].Start;
TTask.WaitForAny(tasks);
ShowMessage ('First task done: ' + value.ToString);
end;
As you can see, we’re creating two tasks:
- the first will sleep for 3000 milliseconds (3 seconds) and then add 3000 to a local variable called value
- the second will sleep for 5000 milliseconds and then add 5000 to value
We start them both and then wait for the first one to finish (on the line where we call TTask.WaitForAny(tasks))
So what will the variable “value” hold immediately after WaitForAny returns? That should be easy. Even allowing for scheduling differences, it should be 3000.
But what you may not realise is the second task is still running. If you don’t believe me, put a breakpoint on the call to Tinterlocked.Add in the second task, debug the method and then leave the ShowMessage dialog up for longer than 2 seconds (giving the second task time to finish) and your breakpoint should fire.

This is important to understand. Depending on your app, and what you are doing in your tasks, this might matter a lot. Let’s say we were doing something else with “value” after the call to ShowMessage. It’s possible that between the call to ShowMessage (where value was equal to 3000) and the next line where we were using it, it’s value could become 8000.
So, what should we do if we don’t want this to happen?
Firstly, after WaitForAny returns, we should cancel the other tasks. Some code like the following will loop through all the tasks, cancelling any that have not completed.
TTask.WaitForAny(tasks);
for LTask in tasks do
LTask.Cancel;
ShowMessage ('All done: ' + value.ToString);
where LTask is just a local variable of type ITask.
That’s a good start, but calling Cancel on a Task doesn’t actually cancel the task. That might sound a little odd at first, but all you are really doing is signalling to the task that it should cancel itself. It’s up to the logic inside the anonymous method you pass the task to check for this status.
When should you check it? Depends on what you’re doing, but there are a few likely places:
- Right at the start of your Task. Remember, you might have tasks that have not yet started, due to waiting for threads to become available in the Thread Pool, so if they have been cancelled before they have even started, may as well find that out early and get out before doing any work.
- If the task is long running, you might check it at regular intervals, so you can bail out and stop any time- or resource-consuming activity as early as possible. Let’s say you are in a loop, you might check it every time around the loop, or every x times around the loop, depending on the work you are doing.
- While the above two might be optional in your app, I’d say this one is mandatory. In my opinion you should absolutely check it before you make any changes outside your task, like updating the UI or in this case, writing back to the “value” variable, like so:
sleep (5000); // 5 seconds
if tasks[1].Status <> TTaskStatus.Canceled then
TInterlocked.Add (value, 5000);
So here’s what my revised code looks like in total:
procedure TFormThreading.Button2Click(Sender: TObject);
var
tasks: array of ITask;
value: Integer;
LTask: ITask;
begin
Setlength (tasks ,2);
value := 0;
tasks[0] := TTask.Create(procedure
begin
sleep (3000);
if tasks[0].Status <> TTaskStatus.Canceled then
TInterlocked.Add(value, 3000);
end);
tasks[0].Start;
tasks[1] := TTask.Create(procedure
begin
sleep (5000);
if tasks[1].Status <> TTaskStatus.Canceled then
TInterlocked.Add (value, 5000);
end);
tasks[1].Start;
TTask.WaitForAny(tasks);
for LTask in tasks do
LTask.Cancel;
ShowMessage('All done: ' + value.ToString);
end;
My main point with this post was to highlight that Tasks continue on, even if you’re not waiting for them anymore, so checking for the Cancelled flag and ending your tasks as soon as possible is a good thing. However, we’re not quite done here. There is still a potential race condition in this code, which I’ll resolve in the next post.
If you’re looking for more details on the PPL, including some meatier examples, check out Danny Wind‘s CodeRage 9 session, and stay all the way to the end.
5 Comments
Most interesting here is that value doesn’t show up in the debugger and you can’t inspect it at all. (Put a break point on value := 0). In fact, it seems any value magically sucked into the anonymous context is unavailable to the debugger.
Hmmm, I”m not seeing that problem. I can use the tooltip evaluations, the watch window, right-click and Inspect, and even the Local Variables window shows Self.Value.
What version of XE7 are you running?
Cheers
Malcolm
There is still a race condition. If the cancel flag is set after the test but before the interlocked.Add, however small this possibility is, it will result in an additional add.
Yes, it’s actually slightly larger than that as well. If the second task finishes after WaitForAny returns and before I set the Cancel flag, I have an issue. I have another post coming on that.
Cheers
Malcolm
…and here it is 🙂 http://www.malcolmgroves.com/blog/?p=1730