.
NET4.0并行计算技术基础(9)
这是一个系列讲座,前面几讲的链接为:
.NET 4.0 并行计算技术基础(1)
.NET 4.0 并行计算技术基础(2)
.
NET 4.0并行计算技术基础(3)
.
NET4.0并行计算技术基础(4)
.NET4.0并行计算技术基础(5)
.NET 4.0并行计算技术基础(6)
.NET4.0并行计算技术基础(7)
.NET4.0并行计算技术基础(8)
=============================================
19.3.8 任务的取消
TPL提供了多个方式取消一个任务。
1 调用Task.Cancel方法直接取消任务的执行
如果要取消一个正在运行的任务,可以调用Task.Cancel方法,此方法会设置Task对象的IsCancellationRequested属性等于true。在任务函数中,通过检查此属性值就可以知道是否需要取消操作。其代码框架如下:
//需要执行的任务函数
Action taskFunction = delegate()
{
//...代码略
if (Task.Current.IsCancellationRequested)
{
//处理外界的取消请求……
//设置Task的状态为IsCanceled
Task.Current.AcknowledgeCancellation();
return;
}
//...代码略
};
注意在任务函数中如何引用到当前的Task对象。
以下代码先启动任务,然后再取消它:
Task tsk = new Task(taskFunction);
tsk.Start();
//…
tsk.Cancel(); //取消操作
示例项目TaskCancel展示了如何使用Task.Cancel()方法取消一个正在执行的任务。
Task.Cancel()是一个异步方法,它不会等待Task对象完成取消操作,仅仅是设置其IsCancellationRequested属性等于true。
注意:
区分IsCancellationRequested和IsCanceled属性
Task类的IsCancellationRequested和IsCanceled属性“长得很像”,但却是不一样的,前者只是表明Task类的Cancel方法被调用了,而IsCanceled属性表明Task对象处于
Canceled
状态,这是Task对象的3个终止状态之一。
这两个属性有着密切的联系。
(1)
Task.Cancel()方法负责设置IsCancellationRequested属性,而Task. AcknowledgeCancellation()方法负责设置IsCanceled属性。
(2)
Task.Cancel()方法通常是由“外界”调用的,它表明外界希望“你”取消当前的工作。而Task. AcknowledgeCancellation()方法是“你自己”在“内部”调用的,向“外界”表明:我已经停止执行当前的工作任务,我的当前状态为“Canceled”。
(3)
调用Task.AcknowledgeCancellation()方法时,要求自身的IsCancellationRequested属性值为true,否则会抛出一个InvalidOperationException异常。
如果发出取消请求的线程希望等待Task对象完成取消工作,可以改为调用Task.CancelAndWait()方法发出取消请求,这是一个同步方法,只有它返回之后,发出取消请求的线程才能继续执行。
为了避免发生由于Task.CancelAndWait()长久不返回而导致发出取消请求的线程无限期阻塞的情况,可以指定一个等待的最长时间,或者是指定一个CancellationToken,从而允许“取消”这个“等待Task对象完成取消工作”的操作。Task.CancelAndWait()方法有几个重载形式用于这一场景,其中一个“功能最强”的形式如下:
public bool CancelAndWait(
int millisecondsTimeout,
CancellationToken cancellationToken
)
millisecondsTimeout指定等待时间,cancellationToken用于指定一个取消令牌对象。
当“时间到”或者cancellationToken对象的IsCancellationRequested属性值等于True时,CancelAndWait()方法将立刻返回,其返回值为false。
2 使用线程统一取消模型在“外界”直接取消任务
我们在第17章中介绍过.NET 4.0所提供的线程统一取消模型。可以在任务函数中直接监控一个CancellationToken对象而实现任务的取消工作。使用这个方法,无需直接调用Task.Cancel()就可以取消操作。如果读者掌握了线程统一取消模型,那么,要利用它来取消一个由Task启动的工作任务是非常简单的事,这个不妨留为读者的一个练习。
3 取消由Parallel类启动的并行计算
如果并行计算是通过Parallel.Invoke、Parallel.For或Parallel.ForEach方法启动的,因为这里并没有直接地提供Task对象可供调用其Cancel方法,所以TPL采用其他方式来取消这种并行循环。
Parallel.For和Parallel.Invoke可以接收一个ParallelOptions类型的参数,它包容一个CancellationToken对象,因而可以直接使用线程统一取消模型来取消。以下是框架代码:
CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions options = new ParallelOptions{CancellationToken = cts.Token};
Parallel.For(循环起始值,循环终止值, options,i=> 需要并行执行的函数());
//……
// “外界”可通过调用cts.Cancel()请求终止并行计算任务
示例项目ParallelInvokeCancel展示了如何使用线程统一取消模型来取消并行计算任务:
示例是一个
Windows Form
项目,使用
Parallel.Invoke
启动了
3
个并行工作任务,在任务函数中,监控
CancellationToken
,当发现有“取消”请求时,工作任务抛出一个
OperationCanceledException
异常给外界。
请注意一下示例程序是如何捕获并处理异常的,下一小节将介绍并行计算中的异常处理机制。
4 并行循环的取消
Parallel.For
和
Parallel.ForEach
启动的是一个并行循环,它们都提供了多个重载的形式,以下列出了
Parallel.For
的一个函数重载形式:
public static ParallelLoopResult For(
int fromInclusive, int toExclusive,
ParallelOptions parallelOptions,
ActionParallelLoopState > body);
在上述函数声明中可以看到,
Parallel.For
可以接收一个
ParallelOptions
类型的参数,因此,我们可以在并行循环中通过监控
CancellationToken
来检查是否外界提出了“取消”请求,但对“取消”请求的处理方式与前面介绍的
Parallel.Invoke
略有不同。
Parallel.Invoke
中途取消任务标准的做法是抛出一个
OperationCanceledException
异常给外界,通知外界任务没有运行结束而中途取消。
在
Parallel.For
的函数声明中,我们看到并行循环体(即上述声明中
body
参数所引用的函数)接收一个
ParallelLoopState
类型的参数,这个参数可以用于中途取消或停止并行循环。
在启动并行循环时,任务并行库会为每一个执行并行循环体的线程关联上一个独立的
ParallelLoopState
对象,通过调用此对象的
Stop()
方法来停止并行循环。
如果在一个并行循环中调用了
ParallelLoopState.Stop()
方法,那么,任务并行库将不会再创建新线程来执行并行循环。等到当前所有正在执行此并行循环的线程终止时,整个并行循环将“优雅”地退场,不会引发异常。
当并行循环以这种方式“提前结束”时,
Parallel.For
和
Parallel.ForEach
方法返回值(是一个
ParallelLoopResult
类型的变量)的
IsCompleted
属性等于
false
,如果并行循环正常完成,
IsCompleted
属性等于
true
。
一个问题出现了:
一个线程通过调用ParallelLoopState.Stop()方法中止了它所执行的并行循环,Parallel.For 和Parallel.ForEach方法创建的其他相关的线程如何知道发生了这件事?
回答:
任务并行库还没有“聪明”到这种能“自动感知”的程度,必须由软件工程师来做这件事。
请注意
ParallelLoopState
类型有一个
IsStopped
属性可以用于“通知”其他的线程。只要这些线程在执行自己的工作时定期检查一下此属性,它就知道是否并行循环中有一个线程中止了此并行循环。
以下是一个框架代码:
ParallelOptions opt = new ParallelOptions();
Parallel.For(0, TaskCount,opt, (int i, ParallelLoopState state) =>
{
//检测一下是否需要取消并行循环
if(opt.CancellationToken.IsCancellationRequested)
{
state.Stop(); //中止并行循环。
return;
}
//检测其他线程是否已中止并行循环
if (state.IsStopped)
{
//提前中止并行循环需要执行的代码
return;
}
//...(其他代码略)
}
除了
ParallelLoopState.Stop
方法,还有一个
ParallelLoopState.Break()
方法也能提前中止一个并行循环,但它只是说:
在完成当前的这轮工作之后,不再执行后继的工作,但在当前这轮工作开始之前“已经在执行”的工作,则必须完成
。而使用
ParallelLoopState.Stop
方法时,不但不会再创建新的线程执行并行循环,而且当前“已经在执行”的工作也应该被中止。
另外需要指出,虽然
ParallelLoopState.Stop
方法会“终结”所有“当前”和“以后”的工作任务,但这并不意味着任何一个线程一调用用
ParallelLoopState.Stop
方法就会立即中止并行循环。具体退出的时机取决于任务调度器和软件工程师所写的处理逻辑。
一般情况下,线程在“主动”调用
ParallelLoopState.Stop()
或
ParallelLoopState.Break()
之后,推荐使用
return
语句结束自己。
Stop
和
Break
的方法的区别非常微妙,需要仔细体会,可以简单地用两句话来表达:
n
ParallelLoopState.Stop
方法中止“当前”及“以后”的工作任务,会导致
ParallelLoopState
对象的
IsStop
属性值等于
true
。
n
ParallelLoopState.Break()
方法仅中止“以后”的工作任务,会导致
ParallelLoopState
对象的
LowestBreakIteration
属性值等于
true
。
注意:
对于嵌套的并行循环,即使在最底层的循环中调用了Break,也会导致整个上层的循环不再执行后继操作,同时,并行库会将ParallelLoopState对象的LowestBreakIteration属性设置为true。
对于嵌套的并行循环,即使在最底层的循环中调用了Break,也会导致整个上层的循环不再执行后继操作,同时,并行库会将ParallelLoopState对象的LowestBreakIteration属性设置为true。
那么,在实际开发中我们如何知道并行循环是正常结束还是提前终止?
以下给出整个判断逻辑:
以下给出整个判断逻辑:
当并行循环顺利完成时
,Parallel.For()
和
Parallel.ForEach()
方法返回值的
IsCompleted=true
。
当
IsCompleted=false
时
,
说明并行循环没有执行完成,需要检查并行循环体函数的
ParallelLoopState
参数的
LowestBreakIteration
属性才能得知原因。
(1)
如果它的
LowestBreakIteration.HasValue=false,
表明是并行循环是因为
ParallelLoopState.Stop
方法被调用而终止的。
(2)
如果它的
LowestBreakIteration.HasValue=true,
表明是并行循环是因为
ParallelLoopState. Break
方法被调用而终止的。
另外,除了
ParallelLoopState.Stop
或
ParallelLoopState. Break
方法可以中止一个并行循环这种方式,未捕获异常也会导致并行循环中止,这时,
ParallelLoopState
的
IsExceptional
属性为
true
。
交叉链接:
19.3.7
节中将介绍如何处理并行程序中的异常。
总之,要编写一个健壮的并行循环,必须在并行循环体中检测
ParallelLoopState
对象的
IsExceptional, IsStopped
和
LowestBreakIteration
三个属性,出于简化编程的目的,
ParallelLoopState
提供了一个
ShouldExitCurrentIteration
属性,当上述
3
个属性中的任何一个值等于
true
时,
ShouldExitCurrentIteration
属性值也为
true
。
本节示例
ParallelLoopStop
集中展示了中止并行循环的编程方法(
图
19‑19
)。
如
图
19‑19
所示,示例程序将启动最多
5
个并行循环,示例程序用以下方法来模拟真实程序中可能“随机”发生的“终止并行循环”请求:
每个并行循环在创建时都会随机生成一个整数
flag
,如果这个整数可以被
3
整除,则此并行循环将负责发出“终止并行循环”请求。
在
图
19‑19
中,我们看到线程3
生成的
flag=87
,可以被
3
整除,符合要求,因此,它调用
ParallelLoopState.Stop
方法发出“终止并行循环”请求,已创建并在运行中的线程4
和
1
都会自动终止。
如果将示例程序中的
ParallelLoopState.Stop()
方法改为
ParallelLoopState.Break ()
方法,或者在某个并行循环中抛出一个未捕获的异常,则程序的运行结果是不一样的。
请读者仔细研究一下这个示例,并动手修改一下代码,并仔细比对以不同方式中止并行循环时示例程序的输出结果,从中可以掌握取消并行循环的基本编程方法。
===================================
下一讲,介绍如何处理并行计算中的异常
《.NET4.0并行计算技术基础(10)》