3. Interrupting a Loop
In a normal C# for or foreach loop, you can break or continue loop iteration by using the break and continue statements, respectively. The break statement interrupts the current iteration and cancels any remaining loop iterations. The continue statement skips the balance of the current iteration but continues with the remaining iterations. Because Parallel.For and Parallel.ForEach don’t execute sequentially, cancellation is a more complex operation. Specifically, you cannot use the break or continue statement in a parallel for loop. This is because Parallel.For and Parallel.ForEach are methods and not language-intrinsic loops. Instead, there are special constructs for canceling a parallel loop.
To interrupt a loop, you need to pass a ParallelLoopState object as the second parameter of the Action delegate used for the parallel operation. You can then interrupt a parallel loop with the ParallelLoopState.Break
method. At that time, other tasks might have completed, be running, or
not have started. For a long-running task, you should periodically
check for a pending interruption. To confirm a pending interruption,
check the ParallelLoopState.ShouldExitCurrentIteration property. If it’s true, there is a pending cancelation. You can find the index of the cancellation task in the ParallelLoopState.LowestBreakIteration
property. Tasks with a higher index value should voluntarily cancel at
a convenient time. Tasks with lower indexes can run to completion.
Tasks not started but with a lower indexed value should be allowed to
start and run to completion, but tasks with higher indexes that have
not started should never run.
The following image illustrates the ParallelLoopState.Break method. This example is a sample scenario. The results might vary based on several factors. In Phase 1, the Parallel.For
method queues six tasks to the .NET Framework 4 thread pool, and Tasks
1, 2, 4, and 5 start running. Available processor cores are not
available for Tasks 3 and 6. At the end of Phase 2, Task 4 calls the ParallelLoopState.Break
method to cancel the loop. In Phase 3, Tasks 1, 2, and 3 are allowed to
complete despite the cancellation, because those tasks have a lower
index value than the canceling task. For that reason, Task 3 is allowed
to start and stop. Task 5 detects the cancellation and voluntarily
stops. Because Task 6 has an index value greater than the cancellation
index, it is not even allowed to start.
The ParallelLoopState.Stop method is an alternative cancellation model. Using this method, all running tasks are expected to cancel as soon as conveniently possible. Running tasks can confirm cancellation with the ParallelLoopState.IsStopped method. When the ParallelLoopState.Stop property is true, tasks are expected to voluntarily stop as soon as possible. Unlike the ParallelLoopState.Break method, unstarted tasks are not allowed to run, regardless of their index value. For these reasons, with the ParallelLoopState.Stop model, fewer tasks are allowed to start or continue when compared to ParallelLoopState.Break. This is a cleaner cancellation model.
The next image illustrates the ParallelLoopState.Stop
method. In Phase 1, six tasks are scheduled but not started. Tasks 1,
2, 4, and 5 are running in Phase 2. At that point, Task 4 calls the ParallelLoopState.Stop method. Tasks 1, 2, and 5 eventually notice the cancellation and stop. There are no tasks running at the end of Phase 3.
In this example, you will start and cancel a Parallel.For loop, reading the cancellation index from the command line.
Create and cancel a Parallel.For loop
-
Create a console application for C# in Visual Studio 2010. With using statements, add the System.Threading.Tasks and System.Threading.Tasks namespaces to the source file.
-
Before the Main method, create a static function named HalfOperation. This function will represent half of the operation for each iteration. The function has no parameters and returns void. In the function, call Thread.SpinWait for half of the maximum int value.
static void HalfOperation()
{
Thread.SpinWait(int.MaxValue / 2);
}
-
In the Main method, you need to convert the first command-line parameter to an index value for cancellation. The int.TryParse
method is convenient and avoids raising an exception for invalid values
or miscasts. If the parameter contains an invalid value, just return.
int cancelValue;
if(!int.TryParse(args[0], out cancelValue))
{
return;
}
-
Start a parallel loop
with a minimum value of 0 and maximum of 12. Create a lambda expression
for the loop operation. Pass the loop index and ParallelLoopState parameter into the lambda expression.
Parallel.For(0, 12, (index, loopState) =>
-
In the lambda expression, display the index of the current task and call the HalfOperation method. Next, check whether this is the cancellation task. If so, call the ParallelLoopState.Break method. Afterward, display a message about the cancellation and stop the current task.
Console.WriteLine("Task {0} started...", index);
HalfOperation();
if (cancelValue == index)
{
loopState.Break();
Console.WriteLine("Loop Operation cancelling. Task {0} cancelled...", index);
return;
}
-
Tasks should periodically check for a cancellation. First check whether a cancellation is pending in the ParallelLoopState.LowestBreakIteration.HasValue
property. If a cancellation is pending, check the cancellation index.
If it’s greater than the index of the current task, end the task. Of
course, display appropriate messages.
if (loopState.LowestBreakIteration.HasValue)
{
if (index > loopState.LowestBreakIteration)
{
Console.WriteLine("Task {0} cancelled", index);
return;
}
}
HalfOperation();
Console.WriteLine("Task {0} ended.", index);
Here is the complete code.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ParallelLoopBreak
{
class Program
{
static void HalfOperation()
{
Thread.SpinWait(int.MaxValue / 2);
}
static void Main(string[] args)
{
int cancelValue;
if(!int.TryParse(args[0], out cancelValue))
{
return;
}
Parallel.For(0, 20, (index, loopState) =>
{
Console.WriteLine("Task {0} started...", index);
HalfOperation();
if (cancelValue == index)
{
loopState.Break();
Console.WriteLine(
"Loop Operation cancelling. " +
"Task {0} cancelled...", index);
return;
}
if (loopState.LowestBreakIteration.HasValue)
{
if (index > loopState.LowestBreakIteration)
{
Console.WriteLine("Task {0} cancelled", index);
return;
}
}
HalfOperation();
Console.WriteLine("Task {0} ended.", index);
});
Console.WriteLine("Press enter to end");
Console.ReadLine();his
}
}
}
The following image shows some partial output from the application.
Your output should look similar. In this example, cancellation occurred
at Task 12. At that time, several other tasks had already started.
And
the next image shows some output from the end of that same application.
Notice that tasks with an index value less than 12 were allowed to
start or run to completion.