如何使方法取消而不会变得丑陋?
我目前正在改造我们长期以来可以取消的方法。 我正在计划使用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的非平凡方法都应如此)。
这也可以使您在需要的时候(例如在长时间运行/密集型操作之前)进行细化或非细化检查。