【问题标题】:Cancellable "fire and forget" task. Is it bad practice? Is my implementation correct?可取消的“一劳永逸”任务。这是不好的做法吗?我的实现是否正确?
【发布时间】:2017-01-05 11:31:19
【问题描述】:

背景:我正在开发一个应用程序,它本质上是一个用于远程服务器的缓存文件浏览器。当用户单击一个目录时,它将从目录树的本地副本中向他们显示其子目录。然后它还会启动一个“即发即弃”任务,以从服务器检索对视图中目录的更改,并更新缓存和用户正在显示的内容。

private CancellationTokenSource _cts;
private SemaphoreSlim _myTaskBlocker = new SemaphoreSlim(1,1);

public void CancelMyTask()
{
    _cts?.Cancel();
}

public async Task FireAndForgetWithCancel()
{
    await _myTaskBlocker.WaitAsync();
    _cts = new CancellationTokenSource();

    try
    {
        //Some potentially long running code.
        token.ThrowIfCancellationRequested();
    }
    catch(OperationCancelledException){}
    finally
    {        
        _cts.dispose();
        _cts = null;
        _myTaskBlocker.Release();
    }
}

编辑 1:可以这样做吗? SemaphoreSlim 有点像 _cts 上的锁定,所以我不需要在进行更改之前锁定它吗?

编辑 2:所以我得出的结论是,这是一个坏主意,并且无法以我最初希望的方式真正实现。

我的解决方案是让 ViewModel 发送请求并监听更新事件。

  public class Model
    {
        // Thread safe observable collection with AddRange.
        public ObservableCollectionEx<file> Files { get; }

        // A request queue.
        public ActionBlock<request> Requests { get; }
    }

    public class ViewModel
    {
        public ObservableCollectionEx<file> FilesTheUserSees { get; }

        public ViewModel()
        {
            Model.Files.CollectionChanged += FileCollectionChanged;
        }

        public async Task UserInstigatedEvent()
        {
            // Do some stuff to FilesTheUserSees here.

            // Request the model to check for updates. Blocks only as long as it takes to send the message.
            await Model.Requests.SendAsync(new request());
        }

        public void FileCollectionChanged(object sender, CollectionChangedEventArgs e)
        {
            // Check to see if there are any new files in Files.
            // If there are new files that match the current requirements add them to FilesTheUserSees.
        }
    }

需要注意的一些问题是,现在依赖 ObservableCollectionsEx 来实现线程安全,但这是一个可以实现的目标,并且即使它有一些缺点也更容易调试。

【问题讨论】:

  • 您要求进行代码审查?
  • 也许吧?我是 C# 的新手,还不太了解该做什么和不该做什么。我想我是在问这是否是糟糕的设计。
  • 你可以使用hangfire来运行后台作业和fireandforgot hangfire.io

标签: c# async-await cancellationtokensource


【解决方案1】:

这不是对CancellationToken 取消过程的良好使用。您的代码有几个正确使用取消令牌不会遇到的问题:

  1. CancelMyTaskFireAndForgetWithCancel 是相互依赖的,因为它们在内部与 CancellationTokenSource 连接,FireAndForgetWithCancel 创建 销毁,但 CancelMyTask 使用。当CancelMyTask 起作用时,您只有一个小窗口,而当取消起作用时,您没有直接指示。
  2. 如果您多次调用FireAndForgetWithCancelCancelMyTask 只会取消最后一个任务,而其他任务会继续执行。
  3. 由于FireAndForgetWithCancel 创建了自己的CancellationTokenSource 并为自己保留了CancellationToken,因此其他消费者无法使用该令牌,例如触发自己的取消或将其与其他令牌组合。
  4. FireAndForgetWithCancel 中,您抓住OperationCancelledException 而不是让它通过。这意味着任务永远不会处于取消状态。

正确的用法是接受取消令牌作为参数,让调用者处理取消,建议将CancellationTokenSourceCancellationToken分开:

private SemaphoreSlim _myTaskBlocker = new SemaphoreSlim(1,1);

public async Task FireAndForgetWithCancel(CancellationToken cancellationToken)
{
    await _myTaskBlocker.WaitAsync();

    try
    {
        //Some potentially long running code.
        cancellationToken.ThrowIfCancellationRequested();
    }
    finally
    {        
        _myTaskBlocker.Release();
    }
}

调用者将创建一个CancellationTokenSource 以获取取消令牌,或者将通过现有的令牌。这将完全取决于调用者,最好留给它。

【讨论】:

  • 感谢您的详细解答。你介意我澄清一些事情吗?
  • @Gauntlet:你想澄清什么以及如何澄清?
  • 1) 据我了解,空引用运算符使 CancelMyTask() 方法安全,因为如果没有 CancellationTokenSource 存在/任务正在运行,它不会做任何事情? 2) 更好的实现方式是将所有生成的 CancellationTokenSources 存储在列表/队列中,并在添加新的之前对所有这些 CancellationTokenSources 调用取消?
  • @Gauntlet,你是​​对的 1),我会补充我的答案。至于2),对此没有一般的答案。但是您的解决方案似乎过于复杂。如果您想在一个步骤中取消许多操作,通常会重复使用相同的取消令牌。
  • 另外,如果您在不等待返回的Task 的情况下调用此方法,则任何异常都将被忽略(演示here)。出于这个原因,您可能希望至少在方法主体中捕获并记录所有异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-05
  • 2012-06-19
  • 1970-01-01
  • 2016-11-11
  • 2016-07-20
相关资源
最近更新 更多