【问题标题】:Pattern for cancel async method on reentry重新进入时取消异步方法的模式
【发布时间】:2021-02-25 16:36:11
【问题描述】:

我想取消重入时的异步功能,这样工作就不会堆积起来,并且可以防止不需要的工作。 例如我的文件扫描可能需要长达 8 秒,但是当我在 UI 中更改文件夹时,应该取消旧功能。 我看到了带有CancellationToken 的示例,但在我看来代码太多了。

我的方法是这样的,它似乎有效,但它给代码增加了很多混乱。 也许我也错过了TaskCanceledException,它会添加更多代码。

private CancellationTokenSource scanFilesState;
private IList<FileInfo> files;

private async Task ScanFilesAsync(string path)
{
    // stop old running
    this.scanFilesState?.Cancel();
    this.scanFilesState?.Dispose();

    this.scanFilesState = new CancellationTokenSource();

    this.files = await FileHandler.ScanFilesAsync(path, this.scanFilesState.Token);

    this.scanFilesState?.Dispose();
    this.scanFilesState = null;
}

有没有更短或更好的方法?

是否有封装此代码的模式?

【问题讨论】:

  • 您可以考虑将这种杂乱无章的功能封装在 CancelableExecution 类中,并使用 Task RunAsync(Func&lt;CancellationToken, Task&gt; action) 公共 API。有一个实现here,由于对线程安全的额外要求,它有很多代码。在您的情况下,我猜您的所有代码都在 UI 线程上运行,因此可以使实现更简单。
  • 好的,谢谢,这似乎是我需要的。很可能我在 UI 线程上运行,但不确定。为什么是评论而不是真正的回复帖子,所以我可以将其标记为答案?
  • 在 GUI 应用程序中,await 之后的延续在 UI 线程上运行(除非您明确将其配置为不这样做),因此您不必担心一个线程取消CancellationTokenSource 而另一个线程处理它。无论FileHandler.ScanFilesAsync 内部发生什么,关于CTS 的处理都无关紧要。因此,花一些时间编写一个更简单的(单线程)实现是有意义的,您可以随意轻松地自定义它。
  • 我之前的评论本质上只是一个指向现有答案的链接。恕我直言,它本身不值得回答。 :-)
  • 取消是协作功能,它不会取消已经启动的任务,因为这会导致系统不稳定。 Task一旦启动,就没有停止过,你最多可以忽略它

标签: c# asynchronous cancellation-token reentrancy


【解决方案1】:

我似乎不需要之后的清理工作, 并提出了这种方法并包装了 CancellationTokenSource 以确保处理安全。

public class SafeCancellationTokenSource : IDisposable
    {
        private CancellationTokenSource state = new CancellationTokenSource();

        public CancellationTokenSource State => state;

        public CancellationToken Token => State.Token;

        public bool IsCancellationRequested => State.IsCancellationRequested;

        public void Cancel()
        {           
            this.state?.Cancel();
            this.state?.Dispose();
            this.state = new CancellationTokenSource();
        }

        public void Dispose()
        {
            this.state?.Dispose();
            this.state = null;
        }
    }

代码现在看起来像这样

private SafeCancellationTokenSource scanFilesState =  new SafeCancellationTokenSource();
private IList<FileInfo> files;

private async Task ScanFilesAsync(string path)
{   
    this.scanFilesState.Cancel();
    this.files = await FileHandler.ScanFilesAsync(path, this.scanFilesState.Token);
}

编辑: 再次添加对 Dispose() 的调用

【讨论】:

  • 哦,是的,我删除了 Dispose() 调用,因为害怕 ObjectDisposedException,并使用了一次性物品列表,就像在你的链接线程中建议的那样。谢谢,我会删除它。
  • 好的,我删除了我的评论。顺便说一句,当您完全控制CancellationTokenSource 时,不要害怕ObjectDisposedException。仅持有对令牌(CancellationToken)的引用的客户端无法执行任何可能导致抛出ObjectDisposedException 的操作。您需要担心的只是您自己的代码。例如访问已处置的CancellationTokenSourceToken 属性会抛出。
  • 你还应该注意the documentation的这句话:"这个类型实现了IDisposable接口。当你使用完该类型的实例时,你应该将它处理掉直接或间接。[...] 否则,在垃圾收集器调用 CancellationTokenSource 对象的 Finalize 方法之前,它正在使用的资源不会被释放。"
  • Puh,现在我很困惑,在我原来的实现中,我有 Dispose() 调用。根据您评论中的链接,它说,当调用 Cancel() 时不需要 Dispose(),除了 ist 是 LinkedCancellationTokenSource。 [stackoverflow.com/questions/6960520/…。所以我应该再次添加对 Dispose() 的调用?
  • 我建议把它放回去。处理丢弃的源是正确的做法。由于您在此之后立即将引用替换为新的 CancellationTokenSource,因此我看不出以后访问旧的已处理源并引发异常的任何可能性。
猜你喜欢
  • 2014-04-20
  • 1970-01-01
  • 2014-08-26
  • 1970-01-01
  • 2020-01-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多