【问题标题】:What is the best way to cancel operation anywhere in a method?在方法中的任何位置取消操作的最佳方法是什么?
【发布时间】:2020-02-21 07:10:22
【问题描述】:

假设我在一个子例程或函数中有一个长操作,并且我希望能够在“取消标志”设置为真后立即取消(退出子例程或函数)。最好的方法是什么?一种方法是在每行代码之后检查标志,但这不是很优雅。

例如:

dim _CancelFlag as boolean = false

Sub LongOperation()
    dim a as integer

    a = 1
    if _CancelFlag = True Then
        Exit Sub
    End If

    a = 2
    if _CancelFlag = True Then
        Exit Sub
    End If

    'And so on...
End Sub

当然a = 1 仅用于说明目的。假设操作真的很长,直到a = 100 并且不可能将它们放入循环中,我如何从子程序外部触发取消并立即停止它?

我正在考虑将 sub 放入 BackgroundWorkerTask,但我仍然需要检查 sub 内部某处的 CancellationToken。我真的必须在每一行代码之后检查吗?

【问题讨论】:

  • 这能回答你的问题吗? Proper way of cancel execution of a method
  • 是的,你必须这样做。 BackgroundWorker 执行冗长的例程,主线程保持 UI 响应,包括例如通过在其 Click 事件中设置 CancelFlag = Truecancel 任务的按钮。
  • 是的,但是需要在后台工作人员 DoWork 内部捕获取消标志并从内部取消,对吗?如果上面的子例程在 BackgroundWorker 的 DoWork 中,我应该在哪里检查: if worker.CancellationPending = True ?
  • 所有取消令牌示例都在循环内。在循环结束时检查取消标志。这与我的问题不同。
  • 重点是你不应该做你想做的事。您可以Abort 一个线程,但通常建议不要这样做,因为它会使系统处于未知状态。你应该明确地检查和取消的原因是为了让你知道你已经清理了任何需要它的东西。

标签: c# .net vb.net algorithm


【解决方案1】:

这取决于您想要达到的粒度:您希望您的方法取消多少秒?

如果必须“立即”取消,您必须在尽可能多的地方办理登机手续。但是,在一般情况下,只需检查操作的长子步骤之前和之后就足够了。

请记住,如果您必须等待句柄,则必须使用指定超时或取消令牌的适当重载。

此外,您应该在方法的深处传播取消令牌/您的标志,以便及早检测取消请求。

【讨论】:

  • 换句话说,在每一行代码之后检查最精细的粒度对吗?
  • 检查调用您无法控制的长操作的行前后。例如,分配变量实际上没有成本,因此在每次分配后检查是没有用的。
  • (如果我不够清楚,请发布你的代码示例,我会告诉你我在说什么)
  • 是的,我知道。我有几个循环里面的操作和很多低成本的操作。我一定会在循环中检查取消标志。问题是低成本操作。它不会花费太多时间,但就我而言,最好在取消标志打开时尽快取消。这是plc io操作。一旦检测到取消标志就可以停止操作,速度更快,但更好。
  • 我同意。最后一件事:如果您使用标志和多个线程,请务必使用 volatile 模式。
【解决方案2】:

我找到了一种更优雅的方法,尽管它最终确实使用了循环。请让我知道是否有人有更好的解决方案。当我发现其他东西时,我也会更新。

Sub LongOperation()
    dim state as integer = 0

    Do while state < 100
        Select Case state
            Case 0
                a = 1
            Case 1
                a = 2
            Case Else
                Exit do
        End Select

        If _CancelFlag = True Then
            Exit Sub
        End If

        state += 1
    Loop

End Sub

【讨论】:

    【解决方案3】:

    这是我创建的一个示例 Windows 应用程序,用于取消或暂停日志运行任务。

    public partial class Form1 : Form
        {
            updateUI _updateGUI;
            CancellationToken _cancelToken;
            PauseTokenSource _pauseTokeSource;
            public Form1()
            {
                InitializeComponent();
            }
    
    
            delegate void updateUI(dynamic value);
    
            private void btnStartAsync_Click(object sender, EventArgs e)
            {
                _pauseTokeSource = new PauseTokenSource();
                _cancelToken = default(CancellationToken);
    
                _pauseTokeSource.onPause -= _pauseTokeSource_onPause;
                _pauseTokeSource.onPause += _pauseTokeSource_onPause;
    
                Task t = new Task(() => { LongRunning(_pauseTokeSource); }, _cancelToken);
                t.Start();
    
            }
    
            private void _pauseTokeSource_onPause(object sender, PauseEventArgs e)
            {
                var message = string.Format("Task {0} at {1}", e.Paused ? "Paused" : "Resumed", DateTime.Now.ToString());
                this.Invoke(_updateGUI, message);
            }
    
            private async void LongRunning(PauseTokenSource pause)
            {
                _updateGUI = new updateUI(SetUI);
                for (int i = 0; i < 20; i++)
                {
    
                    await pause.WaitWhilePausedAsync();
    
                    Thread.Sleep(500);
                    this.Invoke(_updateGUI, i.ToString() + " => " + txtInput.Text);
    
                    //txtOutput.AppendText(Environment.NewLine + i.ToString());
    
    
                    if (_cancelToken.IsCancellationRequested)
                    {
                        this.Invoke(_updateGUI, "Task cancellation requested at " + DateTime.Now.ToString());
                        break;
                    }
                }
                _updateGUI = null;
            }
    
    
            private void SetUI(dynamic output)
            {
                //txtOutput.AppendText(Environment.NewLine + count.ToString() + " => " + txtInput.Text);
                txtOutput.AppendText(Environment.NewLine + output.ToString());
            }
    
            private void btnCancelTask_Click(object sender, EventArgs e)
            {
                _cancelToken = new CancellationToken(true);
            }
    
            private void btnPause_Click(object sender, EventArgs e)
            {
                _pauseTokeSource.IsPaused = !_pauseTokeSource.IsPaused;
                btnPause.Text = _pauseTokeSource.IsPaused ? "Resume" : "Pause";
            }
    }
    
     public class PauseTokenSource
        {
            public delegate void TaskPauseEventHandler(object sender, PauseEventArgs e);
            public event TaskPauseEventHandler onPause;
    
            private TaskCompletionSource<bool> _paused;
            internal static readonly Task s_completedTask = Task.FromResult(true);
    
            public bool IsPaused
            {
                get { return _paused != null; }
                set
                {
    
                    if (value)
                    {
                        Interlocked.CompareExchange(ref _paused, new TaskCompletionSource<bool>(), null);
                    }
                    else
                    {
                        while (true)
                        {
                            var tcs = _paused;
                            if (tcs == null) return;
                            if (Interlocked.CompareExchange(ref _paused, null, tcs) == tcs)
                            {
                                tcs.SetResult(true);
                                onPause?.Invoke(this, new PauseEventArgs(false));
                                break;
                            }
                        }
                    }
                }
            }
    
            public PauseToken Token
            {
                get
                {
                    return new PauseToken(this);
                }
            }
    
            internal Task WaitWhilePausedAsync()
            {
                var cur = _paused;
    
                if (cur != null)
                {
                    onPause?.Invoke(this, new PauseEventArgs(true));
                    return cur.Task;
                }
                return s_completedTask;
            }
        }
    
        public struct PauseToken
        {
            private readonly PauseTokenSource m_source;
            internal PauseToken(PauseTokenSource source) { m_source = source; }
    
            public bool IsPaused { get { return m_source != null && m_source.IsPaused; } }
    
            public Task WaitWhilePausedAsync()
            {
                return IsPaused ?
                    m_source.WaitWhilePausedAsync() :
                    PauseTokenSource.s_completedTask;
            }
        }
    
        public class PauseEventArgs : EventArgs
        {
            public PauseEventArgs(bool paused)
            {
                Paused = paused;
            }
            public bool Paused { get; private set; }
        }
    

    【讨论】:

    • 谢谢你的分享,但我已经知道这个逻辑了。您长时间运行的任务在 for 循环中。在这种情况下,您的循环中每 500 毫秒检查一次取消令牌。我正在寻找的是当它不在循环内并立即取消时,如果可能的话。
    • @AlbertTobing,没问题的兄弟
    • 这里提供的大部分代码都与暂停任务有关,这不是问题的一部分。您本可以将其缩减为 for (int i = 0; i &lt; 20; i++) { Thread.Sleep(500); if (_cancelToken.IsCancellationRequested) { } } 的关键行,但随后问题明确指出“不可能将 [任务的步骤] 放入循环中。”因此,与此处给出的代码一样多,其中没有一个真正适用于或回答问题。
    【解决方案4】:

    如果您的 LongOperation() 可以很好地拆分为短操作(我假设 a=1、a=2、...、a=100 都相当短),那么您可以将所有短操作包装到任务中,将它们放入进入任务队列并处理该队列,检查任务之间是否请求取消。

    如果 LongOperation() 难以拆分,您可以在单独的专用线程上运行 LongOperation() 并在取消时中止该线程。有些人评论说中止线程很脏并且不被推荐。如果处理得当,其实这并没有那么糟糕。中止线程只会在线程方法中引发 ThradAbortException。因此,如果 LongOperation() 中有 try - catch - finally,则捕获并处理异常,并且 finally 代码是否正确进行了清理,关闭所有句柄,处理等,这应该没问题,没什么好害怕的。

    【讨论】:

    • Thread.Abort() 除外 that bad。除其他问题外,当您中止一个线程时,您将不知道它在长时间运行的任务中被取消的位置,除非您将每个步骤都包含在try { ... } catch (ThreadAbortException) { ... } 中,在这种情况下,我们又回到了问题开始的地方。正如Destroying threads 所说,Abort() 用于停止代码“不是为合作取消而设计的”。真正的解决方案? 为合作取消设计代码。
    • 如你所见,我的第一个建议是取消合作设计;)
    • 只有在不可能的情况下Thread.Abort(),当然,在这种情况下,每个步骤或整个LongOperation() 都必须包装在try { ... } catch { ... } finally { ... } 块中。我知道所有这些例子,但它们似乎围绕着在中止的线程中没有正确处理(包括Thread.BeginCriticalReagion() 等等......)。你能举个例子,Thread.Abort() 在中止线程中完成完整和正确的清理和状态管理处理后会受到怎样的伤害?
    • "只有在不可能的情况下" - 除非它可能的。当我们知道作者同时控制取消者和被取消者时,为什么Abort() 甚至在对话中?此外,您现在提到需要诸如“Thread.BeginCriticalReagion() 等”之类的东西(没有定义“等等”),那么您能看到这个答案如何低估了Abort() 的后果吗?问题不在于Abort() 是否可以安全使用,问题在于它是否像您想象的那样简单且“无所畏惧”。无论如何,跳过所有这些圈子听起来真的像这里的“最佳方式”吗?
    猜你喜欢
    • 1970-01-01
    • 2010-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-16
    • 2013-02-10
    相关资源
    最近更新 更多