如何在不变丑的情况下使方法可以取消?

时间:2013-11-05 14:36:48

标签: c# multithreading cancellation-token

我目前正在改进我们长期运行的方法以便取消。我打算使用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();
    // ...
}

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

8 个答案:

答案 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的非平凡方法一样)。

这也允许您根据需要进行粒度或非粒度检查,即在长时间运行/密集型操作之前。