如何使方法取消而不会变得丑陋?

我目前正在改造我们长期以来可以取消的方法。 我正在计划使用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(); // ... } 

我们正在控制不同的硬件单元,需要同步才能一起工作。

如果这些步骤在某种程度上独立于方法中的数据stream,但不能以并行方式执行,那么下面的方法可能会更好一些:

 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 }; 

接下来呢?

 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); 

我承认这不是很美,但是指导是要么做你所做的事情:

 if (cancellationToken.IsCancellationRequested) { /* Stop */ } 

…或略短一些:

 cancellationToken.ThrowIfCancellationRequested() 

一般来说,如果您可以将取消标记传递给各个步骤,则可以将检查分散到不使​​代码饱和的位置。 你也可以select不检查取消, 如果您正在执行的操作是幂等的,并且不是资源密集型的,则不必在每个阶段都检查取消操作。 最重要的检查时间是在返回结果之前。

如果你将令牌传递给你所有的步骤,你可以这样做:

 public static CancellationToken VerifyNotCancelled(this CancellationToken t) { t.ThrowIfCancellationRequested(); return t; } ... Step1(token.VerifyNotCancelled()); Step2(token.VerifyNotCancelled()); Step3(token.VerifyNotCancelled()); 

当我不得不这样做时,我创build了一个委托来做到这一点:

 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); 

简洁版本:

使用lock()Thread.Abort()调用与关键部分同步。


让我解释一下版本:

通常,当中止线程时,你必须考虑两种types的代码:

  • 长时间运行的代码,如果它完成,你并不在乎
  • 代码,绝对必须运行它的过程

第一种types是我们不在乎的types,如果用户请求中止。 也许我们数到了一千亿,他不在乎了吗?

如果我们使用CancellationToken这样的CancellationToken ,那么我们几乎不会在每一次不重要的代码中进行testing。

 for(long i = 0; i < bajillion; i++){ if(cancellationToken.IsCancellationRequested) return false; counter++; } 

这么难看。 所以对于这些情况, Thread.Abort()是一个天赐之物。

不幸的是,正如有人所说,由于绝对必须运行的primefaces代码,你不能使用Thread.Abort() 。 该代码已经从您的账户中扣除了现金,现在必须完成交易并将资金转移到目标账户。 没有人喜欢钱消失。

幸运的是,我们互相排斥,帮助我们处理这类事情。 和C#使得它很漂亮:

 //unimportant long task code lock(_lock) { //atomic task code } 

还有别的地方

 lock(_lock) //same lock { _thatThread.Abort(); } 

锁的数量总是<=哨兵的数量,因为你也想在不重要的代码上使用哨兵(为了让它更快地中止)。 这使得代码比前哨版本稍微漂亮一些,但是它也使得中止更好,因为它不必等待不重要的事情。


还要注意的是, ThreadAbortException可能会在任何地方引发,即使在finally块中也是如此。 这种不可预测性使Thread.Abort()如此引起争议。

通过locking,你可以通过locking整个try-catch-finally块来避免这种情况。 finally清理是至关重要的,那么整个块就可以locking了。 try块一般做得尽可能短,最好是一行,所以我们不locking任何不必要的代码。

这使得Thread.Abort() ,正如你所说的,less一点邪恶。 但是,您可能不想在UI线程上调用它,因为您现在正在locking它。

如果允许重构,则可以重构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; } 

你可能想看看苹果的NSOperation模式作为例子。 这比简单地取消一个方法更复杂,但它非常强大。

我感到惊讶的是没有人提出处理这个标准的,内置的方法:

 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的非平凡方法都应如此)。

这也可以使您在需要的时候(例如在长时间运行/密集型操作之前)进行细化或非细化检查。