【问题标题】:CancellationToken and CancellationTokenSource-How to use it?CancellationToken和CancellationTokenSource-如何使用?
【发布时间】:2013-12-17 16:04:24
【问题描述】:

我有一个名为“加载”的 UI 按钮。它产生一个线程,而线程又产生一个任务。任务有一个等待,如果它过期,任务将被取消。加载按钮未被禁用,用户可以多次单击它。每次单击时,应取消上一个任务。

我对如何在此处使用 CancellationTokenSource 和 CancellationToken 感到困惑。下面是代码。您能否建议如何使用它以及以下用法是否有任何问题?请不要异步,因为我们还没有。

CancellationTokenSource _source = new CancellationTokenSource();
        public void OnLoad()
        {
            //Does this cancel the previously spawned task?
            _source.Cancel();
            _source.Dispose();
            _source = new CancellationTokenSource();
            var activeToken = _source.Token;
            //Do I need to do the above all the time or is there an efficient way?

            Task.Factory.StartNew(() =>
                {
                    var child = Task.Factory.StartNew(() =>
                        {
                            Thread.Sleep(TimeSpan.FromSeconds(20));
                            activeToken.ThrowIfCancellationRequested();
                        }, activeToken);

                    if (!child.Wait(TimeSpan.FromSeconds(5)))
                    {
                        _source.Cancel();
                    }
                });
        }

注意我需要取消任何以前生成的任务,并且每个生成的任务都应该有一个超时。

【问题讨论】:

  • 我认为有一种内置方法可以在一定超时后取消令牌。
  • @SLaks-因为我在 .NET 4.0 上这行不通
  • 我知道你说没有异步,只是一个参考,你可以使用微软的this nuget package 在 4.0 中使用异步(如果你使用的是 VS 2012 或更高版本)。
  • Task 有什么作用?你能定期检查一下里面的CancellationToken吗?

标签: c# c#-4.0 task-parallel-library


【解决方案1】:

这样就可以了:

    private CancellationTokenSource _cancelTasks;

    // this starts your process
    private void DoStuff()
    {
        _cancelTasks = new CancellationTokenSource();

        var task = new Task(() => { /* your actions here */ }, _cancelTasks.Token);
        task.Start();

        if (!task.Wait(5000)) _cancelTasks.Cancel();
    }

【讨论】:

  • 别忘了处理CancellationTokenSource
  • 还建议先将新的 CancellationTokenSource 保存到局部变量中,然后将其传递,以防它被多个线程多次命中。见@Craig Gidney 回答
【解决方案2】:

首先,如果您使用的是 Visual Studio 2012+,您可以添加 Microsoft.Bcl.Async 包,以便为您的 .NET 4.0 项目添加对 async 和其他高级功能的支持。

如果您使用的是 Visual Studio 2010,则可以使用 ParallelExtensionsExtras 库附带的 WithTimeout 扩展方法。该方法使用 TaskCompletionSource 和一个计时器包装原始任务,如果它过期,则调用SetCancelled

代码是here但实际方法很简单:

    /// <summary>Creates a new Task that mirrors the supplied task but that 
    /// will be canceled after the specified timeout.</summary>
    /// <typeparam name="TResult">Specifies the type of data contained in the 
    /// task.</typeparam>
    /// <param name="task">The task.</param>
    /// <param name="timeout">The timeout.</param>
    /// <returns>The new Task that may time out.</returns>
    public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, 
                                                          TimeSpan timeout)
    {
        var result = new TaskCompletionSource<TResult>(task.AsyncState);
        var timer = new Timer(state => 
                        ((TaskCompletionSource<TResult>)state).TrySetCanceled(),
                        result, timeout, TimeSpan.FromMilliseconds(-1));
        task.ContinueWith(t =>
        {
            timer.Dispose();
            result.TrySetFromTask(t);
        }, TaskContinuationOptions.ExecuteSynchronously);
        return result.Task;
    }

您可以在创建任务后立即使用它:

var myTask=Task.Factory.StartNew(()=>{...})
           .WithTimeout(TimeSpan.FromSeconds(20));

一般来说,您可以通过创建一个 TaskCompletionSource 来创建您想要的行为,该 TaskCompletionSource 调用它的 SetResult、SetCancelled 方法来响应您设置的事件或条件。

【讨论】:

    【解决方案3】:

    您的代码中存在一些使事情变得混乱的错误。

    首先,您使用的是 Thread.Sleep 而不是 Task.Delay 或其他一些基于计时器的方法(如果您无权访问 Task.Delay,我强烈建议您编写自己的方法)。睡眠是一种阻塞等待,不能以取消令牌为条件。结果是宝贵的线程池线程被扣为人质数秒,即使操作被取消。这可能会导致较晚的按钮按下的效果被较早的按钮按下。

    其次,在等待结束时,您将取消 _source 但这指的是 当前 source 的 _value 而不是按下按钮时的值。较早的按钮按下将取消以后的按钮按下效果,而不是它们自己的。

    第三,您在一个线程上处理取消令牌源,同时在另一个线程上竞相取消它。你很幸运你没有得到对象处理的异常。

    第四,在这种情况下使用异步是理想的。不过,您提到您只使用 .Net 4.0。

    解决前三件事应该会使正在发生的事情更容易推理:

    CancellationTokenSource _prevSource = new CancellationTokenSource();
    public void OnButtonPress() {
        var curSource = new CancellationTokenSource();
        _prevSource.Cancel();
        _prevSource = curSource;
    
        MyCustomDelay(TimeSpan.FromSeconds(5), curSource.Token).ContinueWith(t => {
            curSource.Cancel();
        }, TaskContinuationOptions.OnlyOnRanToCompletion);
    
        var r = MyCustomDelay(TimeSpan.FromSeconds(20), curSource.Token).ContinueWith(t => {
            curSource.ThrowIfCancellationRequested();
        }, TaskContinuationOptions.OnlyOnRanToCompletion);
        // after 5 seconds the token r's delay is conditions on is cancelled
        // so r is cancelled, due to the continuation specifying OnlyOnRanToCompletion
        // the ThrowIfCancellationRequested line won't be executed
        // although if we removed the cancel-after-5-seconds bit then it would be
    }
    

    【讨论】:

    • .NET 4,没有Task.Delay
    • @Panagiotis 我将其修改为使用计时器实现一个。阻塞的成本使其值得实施。
    猜你喜欢
    • 2022-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-10
    相关资源
    最近更新 更多