47

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

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

4

8 回答 8

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
};
于 2013-11-05T14:45:47.197 回答
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);
于 2013-11-05T15:09:49.207 回答
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());
于 2013-11-05T14:43:12.400 回答
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);
于 2013-11-05T15:13:05.227 回答
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();
}

锁的数量将始终是<=哨兵的数量,因为您也希望在不重要的代码上拥有哨兵(以使其更快地中止)。这使得代码比哨兵版本稍微漂亮,但它也使中止更好,因为它不必等待不重要的事情。


还要注意的是,它ThreadAbortException可以在任何地方提出,甚至在finally块中。这种不可预测性使得Thread.Abort()争议很大。

使用锁,您只需锁定整个try-catch-finally块即可避免这种情况。如果清理finally是必不可少的,那么可以锁定整个块。try块通常尽可能短,最好是一行,因此我们不会锁定任何不必要的代码。

正如你所说,这使得Thread.Abort()邪恶少了一点。不过,您可能不想在 UI 线程上调用它,因为您现在正在锁定它。

于 2013-11-05T15:30:43.827 回答
2

我有点惊讶没有人提出标准的内置方式来处理这个问题:

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

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

于 2013-11-11T22:11:35.183 回答
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;
}
于 2013-11-05T14:46:19.510 回答
1

您可能想以Apple 的 NSOperation 模式为例。它比简单地取消单个方法更复杂,但它非常强大。

于 2013-11-05T20:27:58.003 回答