【问题标题】:How to make a method cancelable without it becoming ugly?如何使方法可以取消而不变得丑陋?
【发布时间】:2013-11-05 14:36:48
【问题描述】:

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

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

【问题讨论】:

  • 每个步骤是否完全独立于其他步骤?
  • 你能把代码重构为有一个方法Step(int number)吗?这样您就可以从 1 循环到 3 并检查取消令牌是否只请求一次?
  • 查看stackoverflow.com/questions/3632149/…,全面比较各种取消线程的方法。
  • 你能在Thread.Abort()调用和临界区之间使用互斥吗?然后,您只需要将关键代码互斥(可能会保存状态),然后让 Thread.Abort() 杀死其余代码。
  • 这可能没有多大帮助,但值得一提:为它制作一个 Monad。我认为您可以在 C# 中实现一个。您的案例通常是 Monad 教程中的第一个示例(即当一个可能失败并且必须中止链时链接函数/步骤)。看向 Haskell 的 Maybe Monad 的方向。似乎是一个很好的概念契合。

标签: c# multithreading cancellation-token


【解决方案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 的非平凡方法也应该如此)。

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

【讨论】:

    【解决方案2】:

    如果步骤在某种程度上独立于方法中的数据流,但不能并行执行,则以下方法可能更易读:

    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 注意:应该是List&lt;Action&lt;CancellationToken&gt;&gt;,但您的想法是正确的。我在想类似的事情。
    • 就个人而言,我认为这并不是一个很好的解决方案。内置的 ThrowIfCancellationRequested 模式更容易维护(通过潜在的许多嵌套方法)并且更具可读性。更不用说接受CancellationToken 的框架/外部方法可能已经抛出OperationCanceledExceptions,这意味着无论如何您都必须将 Run 方法包装在 try catch 块中。我在下面添加了一个答案。
    • @AndrewHanlon 调用者也可以捕获OperationCanceledException,或者您可以只包装运行部分。我认为您的评论与我的回答没有任何关系。 “这取决于”方法的实现方式。
    【解决方案3】:

    继续呢?

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

    【讨论】:

    • 好答案,这是 Tasks 擅长的。
    • 但这看起来超级难看!非常机器友好,但几乎不可读。
    【解决方案4】:

    我承认这并不漂亮,但指导是要么做你做过的事:

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

    【讨论】:

    • 另外 - 在对重要数据项(例如,复制文件的操作中的每个文件)进行迭代后检查是否取消,并且仅在您可以保证成功清理时检查。
    • 就我而言,这些操作是资源密集型的,但还是个好建议。谢谢=)
    【解决方案5】:

    当我不得不做类似的事情时,我创建了一个委托来做这件事:

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

    【讨论】:

      【解决方案6】:

      短版:

      使用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();
      }
      

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


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

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

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

      【讨论】:

        【解决方案7】:

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

        【讨论】:

        • 我相信返回值也应该反转。
        【解决方案8】:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-21
          • 2018-06-11
          • 2019-10-23
          • 2014-07-18
          • 2010-12-23
          相关资源
          最近更新 更多