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; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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); 1 2 3 4 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); 1 2 3 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; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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.