我目前正在改进我们长期运行的方法以便取消。我打算使用System.Threading.Tasks.CancellationToken来实现它。
我们的方法通常执行一些长时间运行的步骤(主要向硬件发送命令然后等待硬件),例如
void Run()
{
Step1();
Step2();
Step3();
}
我对取消的第一个(也许是愚蠢的)想法会将其转化为
bool Run(CancellationToken cancellationToken)
{
Step1(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return false;
Step2(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return false;
Step3(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return false;
return true;
}
坦率地看起来很可怕。这种“模式”也将在单个步骤内继续(并且它们必然已经相当长)。这会使Thread.Abort()看起来很性感,虽然我知道它不推荐。
是否有一个更清晰的模式来实现这一点,不会隐藏许多样板代码下的应用程序逻辑?
修改
作为步骤性质的示例,Run
方法可以读取
void Run()
{
GiantRobotor.MoveToBase();
Oven.ThrowBaguetteTowardsBase();
GiantRobotor.CatchBaguette();
// ...
}
我们正在控制需要同步的不同硬件单元才能协同工作。
答案 0 :(得分:33)
如果这些步骤以某种方式独立于方法中的数据流,但不能以并行的方式执行,则以下方法可能更易读:
void Run()
{
// list of actions, defines the order of execution
var actions = new List<Action<CancellationToken>>() {
ct => Step1(ct),
ct => Step2(ct),
ct => Step3(ct)
};
// execute actions and check for cancellation token
foreach(var action in actions)
{
action(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return false;
}
return true;
}
如果这些步骤不需要取消令牌,因为你可以用很小的单位拆分它们,你甚至可以写一个较小的列表定义:
var actions = new List<Action>() {
Step1, Step2, Step3
};
答案 1 :(得分:23)
继续?
var t = Task.Factory.StartNew(() => Step1(cancellationToken), cancellationToken)
.ContinueWith(task => Step2(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current)
.ContinueWith(task => Step3(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
答案 2 :(得分:9)
我承认它并不漂亮,但指导就是做你做过的事情:
if (cancellationToken.IsCancellationRequested) { /* Stop */ }
......或略短:
cancellationToken.ThrowIfCancellationRequested()
通常,如果您可以将取消令牌传递给各个步骤,则可以将签出分散,以免它们使代码饱和。您也可以选择不经常检查取消;如果您正在执行的操作是幂等的并且不是资源密集型的,那么您不一定要在每个阶段检查取消。最重要的检查时间是在返回结果之前。
如果您将令牌传递给所有步骤,则可以执行以下操作:
public static CancellationToken VerifyNotCancelled(this CancellationToken t) {
t.ThrowIfCancellationRequested();
return t;
}
...
Step1(token.VerifyNotCancelled());
Step2(token.VerifyNotCancelled());
Step3(token.VerifyNotCancelled());
答案 3 :(得分:7)
当我不得不做这样的事情时,我创建了一个代表来做这件事:
bool Run(CancellationToken cancellationToken)
{
var DoIt = new Func<Action<CancellationToken>,bool>((f) =>
{
f(cancellationToken);
return cancellationToken.IsCancellationRequested;
});
if (!DoIt(Step1)) return false;
if (!DoIt(Step2)) return false;
if (!DoIt(Step3)) return false;
return true;
}
或者,如果步骤之间从未有任何代码,您可以写:
return DoIt(Step1) && DoIt(Step2) && DoIt(Step3);
答案 4 :(得分:4)
使用lock()
将Thread.Abort()
电话与关键部分同步。
通常,在中止线程时,您必须考虑两种类型的代码:
如果用户请求中止,则第一种类型是我们不关心的类型。也许我们数到千亿,而他又不在乎了?
如果我们使用诸如CancellationToken
之类的标记,我们很难在不重要的代码的每次迭代中测试它们吗?
for(long i = 0; i < bajillion; i++){
if(cancellationToken.IsCancellationRequested)
return false;
counter++;
}
太丑了。因此,对于这些情况,Thread.Abort()
是天赐之物。
不幸的是,正如一些人所说的那样,你不能使用Thread.Abort()
因为必须运行的原子代码!代码已经从您的帐户中扣除了金额,现在它必须完成交易并将资金转移到目标帐户。没有人喜欢钱消失。
幸运的是,我们互相排斥,帮助我们解决这类问题。而且C#很漂亮:
//unimportant long task code
lock(_lock)
{
//atomic task code
}
以及其他地方
lock(_lock) //same lock
{
_thatThread.Abort();
}
锁的数量永远是<=
个哨兵的数量,因为你也想要在不重要的代码上使用哨兵(以使其中止更快)。这使代码比sentinel版本稍微漂亮,但它也使得中止更好,因为它不必等待不重要的事情。
另外需要注意的是ThreadAbortException
可以在任何地方引发,即使在finally
块中也是如此。这种不可预测性使得Thread.Abort()
如此具有争议性。
使用锁定,只需锁定整个try-catch-finally
块就可以避免这种情况。它finally
中的清理是必不可少的,然后可以锁定整个块。 try
块通常尽可能短,最好一行,所以我们不会锁定任何不必要的代码。
这使得Thread.Abort()
,正如你所说,有点不那么邪恶。但是,您可能不想在UI线程上调用它,因为您现在正在锁定它。
答案 5 :(得分:1)
如果允许使用重构,则可以重构Step方法,以便有一种方法Step(int number)
。
然后,您可以从1循环到N,并检查是否只请求取消令牌一次。
bool Run(CancellationToken cancellationToken) {
for (int i = 1; i < 3 && !cancellationToken.IsCancellationRequested; i++)
Step(i, cancellationToken);
return !cancellationToken.IsCancellationRequested;
}
或等价地:(无论您喜欢哪种)
bool Run(CancellationToken cancellationToken) {
for (int i = 1; i < 3; i++) {
Step(i, cancellationToken);
if (cancellationToken.IsCancellationRequested)
return false;
}
return true;
}
答案 6 :(得分:1)
您可能需要查看Apple's NSOperation pattern作为示例。它比简单地使单个方法可取消更复杂,但它非常强大。
答案 7 :(得分:1)
我很惊讶没有人提出标准的,内置的处理方法:
bool Run(CancellationToken cancellationToken)
{
//cancellationToke.ThrowIfCancellationRequested();
try
{
Step1(cancellationToken);
Step2(cancellationToken);
Step3(cancellationToken);
}
catch(OperationCanceledException ex)
{
return false;
}
return true;
}
void Step1(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
...
}
虽然通常你不想依赖于将检查推送到更深层次,但在这种情况下,步骤已经接受了CancellationToken并且应该进行检查(正如任何接受CancellationToken的非平凡方法一样)。
这也允许您根据需要进行粒度或非粒度检查,即在长时间运行/密集型操作之前。