简体   繁体   English

如何在 TPL 中处理任务取消

[英]How to handle task cancellation in the TPL

Good day!再会! I am writing a helper library for WinForms UI.我正在为 WinForms UI 编写一个帮助程序库。 Started using TPL async/await mechanism and got a problem with this kind of code example :开始使用 TPL async/await 机制并遇到这种代码示例的问题:

    private SynchronizationContext _context;

    public void UpdateUI(Action action)
    {
        _context.Post(delegate { action(); }, null);
    }


    private async void button2_Click(object sender, EventArgs e)
    {

        var taskAwait = 4000;
        var progressRefresh = 200;
        var cancellationSource = new System.Threading.CancellationTokenSource();

        await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });

        Action usefulWork = () =>
        {
            try
            {
                Thread.Sleep(taskAwait);
                cancellationSource.Cancel();
            }
            catch { }
        };
        Action progressUpdate = () =>
        {
            int i = 0;
            while (i < 10)
            {
                UpdateUI(() => { button2.Text = "Processing " + i.ToString(); });
                Thread.Sleep(progressRefresh);
                i++;
            }
            cancellationSource.Cancel();
        };

        var usefulWorkTask = new Task(usefulWork, cancellationSource.Token);
        var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token);

        try
        {
            cancellationSource.Token.ThrowIfCancellationRequested();
            Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token);
            Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token);
            await Task.Run(() =>
            {
                try
                {
                    var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);                        
                }
                catch { }
            }).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
        }
        await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); });
    }

Basically, the idea is to run two parallel tasks - one is for, say, progress bar or whatever update and a sort of timeout controller, the other is the long running task itself.基本上,这个想法是运行两个并行任务 - 例如,进度条或任何更新和一种超时控制器,另一个是长时间运行的任务本身。 Whichever task finishes first cancels the other one.无论哪个任务先完成都会取消另一个任务。 So, there should not be a problem to cancel the "progress" task as it has a loop in which I can check if task is marked cancelled.因此,取消“进度”任务应该没有问题,因为它有一个循环,我可以在其中检查任务是否标记为已取消。 The problem is with the long running one.问题在于长期运行的。 It could be Thread.Sleep() or SqlConnection.Open().它可以是 Thread.Sleep() 或 SqlConnection.Open()。 When I run CancellationSource.Cancel(), the long running task keeps working and does not cancel.当我运行 CancellationSource.Cancel() 时,长时间运行的任务会继续工作并且不会取消。 After a timeout I am not interested in long running task or whatever it may result in.超时后,我对长时间运行的任务或任何可能导致的结果不感兴趣。
As the cluttered code example may suggest, I have tried a bunch of variants and none given me a desired effect.正如杂乱的代码示例所暗示的那样,我尝试了很多变体,但没有一个给我想要的效果。 Something like Task.WaitAny() freezes UI... Is there a way to make that cancellation work or may be even a different approach to code these things?像 Task.WaitAny() 之类的东西会冻结 UI... 有没有办法让取消工作,或者甚至可能是一种不同的方法来编码这些东西?

UPD:更新:

public static class Taskhelpers
{
    public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        {
            if (task != await Task.WhenAny(task, tcs.Task))
                throw new OperationCanceledException(cancellationToken);
        }
        return await task;
    }
    public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
        {
            if (task != await Task.WhenAny(task, tcs.Task))
                throw new OperationCanceledException(cancellationToken);
        }
        await task;
    }
}

..... .....

        var taskAwait = 4000;
        var progressRefresh = 200;
        var cancellationSource = new System.Threading.CancellationTokenSource();
        var cancellationToken = cancellationSource.Token;

        var usefulWorkTask = Task.Run(async () =>
        {
            try
            {
                System.Diagnostics.Trace.WriteLine("WORK : started");

                await Task.Delay(taskAwait).WithCancellation(cancellationToken);

                System.Diagnostics.Trace.WriteLine("WORK : finished");
            }
            catch (OperationCanceledException) { }  // just drop out if got cancelled
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message);
            }
        }, cancellationToken);

        var progressUpdatetask = Task.Run(async () =>
        {
            for (var i = 0; i < 25; i++)
            {
                if (!cancellationToken.IsCancellationRequested)
                {
                    System.Diagnostics.Trace.WriteLine("==== : " + i.ToString());
                    await Task.Delay(progressRefresh);
                }
            }
        },cancellationToken);

        await Task.WhenAny(usefulWorkTask, progressUpdatetask);

        cancellationSource.Cancel();

By modifying for (var i = 0; i < 25; i++) limit of i I imitate whether long running task finishes before the progress task or otherwise.通过修改for (var i = 0; i < 25; i++) limit of i我模拟长时间运行的任务是否在进度任务之前完成或以其他方式完成。 Works as desired.根据需要工作。 The WithCancellation helper method does the job, although two sort of 'nested' Task.WhenAny look suspicious for now. WithCancellation辅助方法可以完成这项工作,尽管两种“嵌套”的Task.WhenAny现在看起来很可疑。

I agree with all the points in Paulo's answer - namely, use modern solutions ( Task.Run instead of Task.Factory.StartNew , Progress<T> for progress updates instead of manually posting to the SynchronizationContext , Task.WhenAny instead of Task.WaitAny for asynchronous code).我同意 Paulo 回答中的所有要点 - 即使用现代解决方案( Task.Run而不是Task.Factory.StartNewProgress<T>进行进度更新而不是手动发布到SynchronizationContextTask.WhenAny而不是Task.WaitAny对于异步代码)。

But to answer the actual question:但要回答实际问题:

When I run CancellationSource.Cancel(), the long running task keeps working and does not cancel.当我运行 CancellationSource.Cancel() 时,长时间运行的任务会继续工作并且不会取消。 After a timeout I am not interested in long running task or whatever it may result in.超时后,我对长时间运行的任务或任何可能导致的结果不感兴趣。

There are two parts to this:这有两个部分:

  • How do I write code that responds to a cancellation request?如何编写响应取消请求的代码?
  • How do I write code that ignores any responses after the cancellation?如何编写在取消后忽略任何响应的代码?

Note that the first part deals with cancelling the operation , and the second part is actually dealing with cancelling the waiting for the operation to complete .请注意,第一部分处理取消操作,第二部分实际上处理取消等待操作完成

First things first: support cancellation in the operation itself.第一件事:支持操作本身的取消。 For CPU-bound code (ie, running a loop), periodically call token.ThrowIfCancellationRequested() .对于 CPU 绑定代码(即运行循环),定期调用token.ThrowIfCancellationRequested() For I/O-bound code, the best option is to pass the token down to the next API layer - most (but not all) I/O APIs can (should) take cancellation tokens.对于 I/O 绑定代码,最好的选择是将token向下传递到下一个 API 层 - 大多数(但不是全部)I/O API 可以(应该)获取取消令牌。 If this isn't an option, then you can either choose to ignore the cancellation, or you can register a cancellation callback with token.Register .如果这不是一个选项,那么您可以选择忽略取消,或者您可以使用token.Register注册取消回调。 Sometimes there's a separate cancellation method you can call from your Register callback, and sometimes you can make it work by disposing the object from the callback (this approach often works because of a long-standing Win32 API tradition of cancelling all I/O for a handle when that handle is closed).有时,您可以从Register回调中调用一个单独的取消方法,有时您可以通过从回调中处理对象来使其工作(这种方法通常有效,因为长期存在的 Win32 API 传统是取消所有 I/O手柄关闭时的手柄)。 I'm not sure if this will work for SqlConnection.Open , though.不过,我不确定这是否适用于SqlConnection.Open

Next, cancelling the wait .接下来,取消等待 This one is relatively simple if you just want to cancel the wait due to a timeout :如果您只是想因超时而取消等待,则此方法相对简单:

await Task.WhenAny(tWork, tProgress, Task.Delay(5000));

When you write something like await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });当你写类似await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); }); on your button2_Click , you are, from the UI thread, scheduling an action to a thread poll thread that posts an action to the UI thread.button2_Click ,您从 UI 线程将操作调度到线程轮询线程,该线程轮询线程将操作发布到 UI 线程。 If you called the action directly, it would be quickier because it wouldn't have two context switchings.如果您直接调用操作,它会更快,因为它不会有两个上下文切换。

ConfigureAwait(false) causes the synchronization context to not being captured. ConfigureAwait(false)导致未捕获同步上下文。 I should not be used inside UI methods because, you most certainely, want to do some UI work on the continuation.我不应该在 UI 方法中使用,因为你肯定想在延续上做一些 UI 工作。

You shouldn't use Task.Factory.StartNew instead of Task.Run unless you absolutely have a reason to.除非绝对有理由,否则不应使用Task.Factory.StartNew而不是Task.Run See this and this .看到这个这个

For progress updates, consider using the Progress<T> class , because it captures the synchronization context.对于进度更新,请考虑使用Progress<T> 类,因为它捕获同步上下文。

Maybe you should try something like this:也许你应该尝试这样的事情:

private async void button2_Click(object sender, EventArgs e)
{
    var taskAwait = 4000;
    var cancellationSource = new CancellationTokenSource();
    var cancellationToken = cancellationSource.Token;
    
    button2.Text = "Processing...";
    
    var usefullWorkTask = Task.Run(async () =>
        {
            try
            {
                await Task.Dealy(taskAwait);
            }
            catch { }
        },
        cancellationToken);
    
    var progress = new Progress<imt>(i => {
        button2.Text = "Processing " + i.ToString();
    });

    var progressUpdateTask = Task.Run(async () =>
        {
            for(var i = 0; i < 10; i++)
            {
                progress.Report(i);
            }
        },
        cancellationToken);
        
    await Task.WhenAny(usefullWorkTask, progressUpdateTask);
    
    cancellationSource.Cancel();
}

I think you need to check IsCancellationRequested in the progressUpdate Action.我认为您需要在progressUpdate操作中检查IsCancellationRequested

As to how to do what you want, this blog discusses an Extension method WithCancellation that will make it so that you stop waiting for your long running task.至于如何做你想做的事, 这篇博客讨论了一个扩展方法WithCancellation ,它可以让你停止等待长时间运行的任务。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM