CancellationToken和CancellationTokenSource-如何使用?

9
我有一个名为 Load 的用户界面按钮,它会生成一个线程,该线程进而生成一个任务。任务中有一个等待操作,如果超时,则任务被取消。Load 按钮不会被禁用,用户可以多次点击它。每次点击时,上一个任务都应该被取消。
我对如何在此处使用 CancellationTokenSource 和 CancellationToken 感到困惑。以下是代码。您能否建议如何使用,并且下面的用法是否有问题?请勿使用 Async,因为我们还没有到那里。
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();
                    }
                });
        }

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


我认为有一种内置的方法可以在特定的超时时间后取消一个令牌。 - CodesInChaos
https://dev59.com/DWQn5IYBdhLWcg3wvZR_#16607800 - SLaks
@SLaks-那行不通,因为我使用的是.NET 4.0。 - Mike
2
我知道你说不要用异步,但是提醒一下,如果你使用的是VS 2012或更新版本,你可以使用微软的这个nuget包,在4.0中使用异步。 - Scott Chamberlain
Task是做什么的?你能在其中定期检查CancellationToken吗? - svick
可以的。重要的是我需要取消任何先前生成的任务。 - Mike
3个回答

10
这样做就可以:

这样做就可以:

    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();
    }

7
别忘了处理 CancellationTokenSource - Scott
1
还建议先将新的CancellationTokenSource保存到本地变量中,然后传递它,以防止它被多个线程多次调用。请参见@Craig Gidney的答案。 - Michal Ciechan

5

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

如果您正在使用Visual Studio 2010,则可以使用ParallelExtensionsExtras库中提供的WithTimeout扩展方法。该方法将原始任务包装在TaskCompletionSource和一个定时器中,如果超时则调用SetCancelled

代码在这里,但实际方法很简单:

    /// <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 对象,并在您设置的事件或条件触发时调用其 SetResult、SetCancelled 方法来实现您想要的行为。


2
你的代码中存在一些错误,这使得事情变得混乱。
首先,你使用了Thread.Sleep而不是Task.Delay或其他基于定时器的方法(如果你没有访问Task.Delay,我强烈建议你编写自己的方法)。Sleep是一个阻塞等待,不能根据取消令牌进行条件设置。结果是宝贵的线程池线程被扣押多秒钟,即使操作被取消。这可能会导致后面的按钮按下效果被早期的按钮按下所拖延。
其次,在等待结束时,你取消_source,但这是指源的当前_value,而不是按下按钮时的值。之前的按钮按下将取消后续的按钮按下效果,而不是它们自己的效果。
第三,你在一个线程上处置了取消令牌源,而在另一个线程上却试图取消它。你很幸运没有得到对象已释放异常。
第四,在这种情况下最理想的是使用async。尽管你提到你只使用.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
}

1
.NET 4 中没有 Task.Delay。 - Panagiotis Kanavos
@Panagiotis 我修改了它,让它说要实现一个带有计时器的版本。阻塞的成本使得实现它变得值得。 - Craig Gidney

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接