【问题标题】:How to cancel a deeply nested process如何取消深度嵌套的进程
【发布时间】:2011-03-01 23:13:47
【问题描述】:

我有一个“经理”类的课程。它的功能之一是指示类的长时间运行的进程应该关闭。它通过在类中设置一个名为“IsStopping”的布尔值来做到这一点。

public class Foo
{
    bool isStoping

    void DoWork() {
        while (!isStopping)
        {
            // do work...
        }
    }
}

现在,DoWork() 是一个巨大的函数,我决定对其进行重构,并作为该过程的一部分将其中的一些分解为其他类。问题是,其中一些类也有长时间运行的函数,需要检查 isStopping 是否为真。

public class Foo
{
    bool isStoping

    void DoWork() {
        while (!isStopping)
        {
            MoreWork mw = new MoreWork()
            mw.DoMoreWork() // possibly long running
            // do work...
        }
    }
}

我有什么选择?

我考虑过通过引用传递 isStopping,但我不太喜欢,因为它需要有一个外部对象。我更愿意让额外的类尽可能独立且无依赖。

我也考虑过创建 isStopping 一个属性,然后让它调用一个可以订阅内部类的事件,但这似乎过于复杂。

另一种选择是创建一个“进程取消令牌”类,类似于 .net 4 任务使用的类,然后将该令牌传递给这些类。

您是如何处理这种情况的?

编辑:

还要考虑 MoreWork 可能有一个 EvenMoreWork 对象,它实例化并调用一个可能长时间运行的方法……等等。我想我正在寻找的是一种能够在调用树下向任意数量的对象发出信号的方法,以告诉他们停止正在做的事情并清理并返回。

EDIT2:

感谢您迄今为止的回复。似乎对使用方法没有真正的共识,每个人都有不同的意见。看来这应该是一种设计模式……

【问题讨论】:

  • 使用线程还是不使用线程?因为,如果这样的事情不是多线程的,IMO 很难看到这种情况。
  • 线程与否在很大程度上无关紧要。例如,它可以由 Windows 消息在具有消息泵的单线程应用程序中设置。但是,是的,我的特定实现使用线程。

标签: c# oop refactoring request-cancelling


【解决方案1】:

我认为触发您的子类订阅的事件是有意义的。

【讨论】:

    【解决方案2】:

    您可以在您的经理类和其他每个工人类上创建一个 Cancel() 方法。基于接口。

    管理器类或实例化其他工作类的类必须将 Cancel() 调用传播到它们所组成的对象。

    然后,最深的嵌套类只需将内部 _isStopping bool 设置为 false,您的长时间运行的任务就会检查这一点。

    或者,您可以创建一个所有类都知道的某种上下文,以及它们可以检查取消标志的位置。

    另一个选择是创建一个 “进程取消令牌”类, 类似于 .net 4 Tasks 使用的,然后 该令牌将传递给这些类。

    我对此并不熟悉,但如果它基本上是一个带有 bool 属性标志的对象,并且你传递给每个类,那么这对我来说似乎是最干净的方式。然后,您可以创建一个抽象基类,该类具有一个构造函数,该构造函数接受 this 并将其设置为私有成员变量。然后您的流程循环可以检查是否取消。 显然,您必须保留对已传递给工作人员的此对象的引用,以便可以从您的 UI 中为其设置布尔标志。

    【讨论】:

      【解决方案3】:

      这里有两种方法:

      1)您已经概述的解决方案:将信号机制传递给您的从属对象:bool(通过 ref),父对象本身隐藏在接口中(在下面的示例中为Foo: IController),或其他.子对象根据需要检查信号。

      // Either in the MoreWork constructor
      public MoreWork(IController controller) {
          this.controller = controller;
      }
      
      // Or in DoMoreWork, depending on your preferences
      public void DoMoreWork(IController controller) {
          do {
              // More work here
          } while (!controller.IsStopping);
      }
      

      2) 把它转过来并使用observer pattern - 它可以让你将你的从属对象与父对象分离。如果我手动操作(而不是使用事件),我会修改我的子类以实现IStoppable 接口,并让我的管理器类告诉他们何时停止:

      public interface IStoppable {
          void Stop();
      }
      
      public class MoreWork: IStoppable {
          bool isStopping = false;
          public void Stop() { isStopping = true; }
          public void DoMoreWork() {
              do {
                  // More work here
              } while (!isStopping);
          }
      }
      

      Foo 维护其可停止对象的列表,并在其自己的停止方法中将它们全部停止:

      public void Stop() {
          this.isStopping = true;
          foreach(IStoppable stoppable in stoppables) {
              stoppable.Stop();
          }
      }
      

      【讨论】:

        【解决方案4】:

        在检查停止标志最明智的地方用这样的语句乱扔代码:

        if(isStopping) { throw new OperationCanceledException(); }
        

        在顶层捕获OperationCanceledException

        这并没有真正的性能损失,因为 (a) 它不会经常发生,并且 (b) 当它确实发生时,它只会发生一次。

        此方法还可以与 WinForms BackgroundWorker 组件配合使用。工作线程将自动捕获工作线程中抛出的异常并将其编组回 UI 线程。您只需检查e.Error 属性的类型,例如:

        private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
            if(e.Error == null) {
                // Finished
            } else if(e.Error is OperationCanceledException) {
                // Cancelled
            } else {
                // Genuine error - maybe display some UI?
            }
        }
        

        【讨论】:

        • 你可以重用OperationCanceledException,而不是创建一个新的异常类。
        • 感谢 CodeSavvyGeek,您为我节省了一些编码。 :-)
        • 如果您使用的是 .net 4,那么有一个非常好的预制结构 msdn.microsoft.com/en-us/library/dd384802.aspx 只需调用 myToken.ThrowIfCancellationRequested(),它就会执行 Christian 发布的上述代码。它还有一些其他不错的功能,例如注册事件,这些事件将被触发以执行一些清理任务。
        • 看,.net 4 CancelationToken 方法困扰着我。我一直认为例外是针对特殊情况,而不是用作 goto。如果您期望发生某些事情(并且结束是始终会发生的预期条件),那么它不应该使用异常。我可以“有点”看到在正常完成之前取消一个过程是例外的,但是为了完成这个过程......我对这种方法不太兴奋。是的,我知道我在帖子中说“取消”,我应该更仔细地描述这两者。我需要两者都做。
        【解决方案5】:

        您的嵌套类型可以接受委托(或公开事件)来检查取消条件。然后,您的经理向嵌套类型提供一个委托,以检查其自己的“shouldStop”布尔值。这样,唯一的依赖关系是 NestedType 上的 ManagerType,反正你已经拥有了。

        class NestedType
        {
            // note: the argument of Predicate<T> is not used, 
            //    you could create a new delegate type that accepts no arguments 
            //    and returns T
            public Predicate<bool> ShouldStop = delegate() { return false; };
            public void DoWork()
            {
                while (!this.ShouldStop(false))
                {
                    // do work here
                }
            }
        }
        
        class ManagerType
        {
            private bool shouldStop = false;
            private bool checkShouldStop(bool ignored)
            {
                return shouldStop;
            }
            public void ManageStuff()
            {
                NestedType nestedType = new NestedType();
                nestedType.ShouldStop = checkShouldStop;
                nestedType.DoWork();
            }
        }
        

        如果你真的想的话,你可以把这个行为抽象成一个接口。

        interface IStoppable
        {
            Predicate<bool> ShouldStop;
        }
        

        此外,您可以让“停止”机制引发异常,而不仅仅是检查布尔值。在管理器的 checkShouldStop 方法中,它可以简单地抛出一个OperationCanceledException

        class NestedType
        {
            public MethodInvoker Stop = delegate() { };
            public void DoWork()
            {
                while (true)
                {
                    Stop();
                    // do work here
                }
            }
        }
        
        class ManagerType
        {
            private bool shouldStop = false;
            private void checkShouldStop()
            {
                if (this.shouldStop) { throw new OperationCanceledException(); }
            }
            public void ManageStuff()
            {
                NestedType nestedType = new NestedType();
                nestedType.Stop = checkShouldStop;
                nestedType.DoWork();
            }
        }
        

        我以前使用过这种技术,发现它非常有效。

        【讨论】:

          【解决方案6】:

          您可以通过使用命令模式将每个 DoWork() 调用转换为命令来展平调用堆栈。在顶层,您维护要执行的命令队列(或堆栈,取决于您的命令如何相互交互)。 “调用”函数被转换为将新命令排入队列。然后,在处理每个命令之间,您可以检查是否取消。喜欢:

          void DoWork() {
              var commands = new Queue<ICommand>();
          
              commands.Enqueue(new MoreWorkCommand());
              while (!isStopping && !commands.IsEmpty)
              {
                  commands.Deque().Perform(commands);
              }
          }
          
          public class MoreWorkCommand : ICommand {
              public void Perform(Queue<ICommand> commands) {
                  commands.Enqueue(new DoMoreWorkCommand());
              }
          }
          

          基本上,通过将低级调用堆栈转换为您控制的数据结构,您可以检查每个“调用”、暂停、恢复、取消等之间的内容。

          【讨论】:

            猜你喜欢
            • 2012-08-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-04-17
            • 2018-03-23
            • 2018-10-17
            相关资源
            最近更新 更多