【问题标题】:How to handle task cancellation in the TPL如何处理 TPL 中的任务取消
【发布时间】:2015-12-07 20:18:49
【问题描述】:

美好的一天!我正在为 WinForms UI 编写一个帮助程序库。开始使用 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"; }); });
    }

基本上,这个想法是运行两个并行任务 - 例如,一个用于进度条或任何更新和一种超时控制器,另一个是长时间运行的任务本身。无论哪个任务先完成,都会取消另一个。因此,取消“进度”任务应该没有问题,因为它有一个循环,我可以在其中检查任务是否标记为已取消。问题在于长期运行的问题。它可以是 Thread.Sleep() 或 SqlConnection.Open()。当我运行 CancellationSource.Cancel() 时,长时间运行的任务会继续工作并且不会取消。超时后,我对长时间运行的任务或它可能导致的任何结果不感兴趣。
正如杂乱的代码示例所暗示的那样,我尝试了一堆变体,但没有一个能达到我想要的效果。诸如 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();

通过修改ifor (var i = 0; i &lt; 25; i++) 限制,我模拟长时间运行的任务是否在进度任务之前完成。根据需要工作。 WithCancellation 辅助方法可以完成这项工作,尽管两种“嵌套”Task.WhenAny 现在看起来很可疑。

【问题讨论】:

    标签: c# async-await task-parallel-library task cancellation


    【解决方案1】:

    我认为您需要在progressUpdate 操作中检查IsCancellationRequested

    至于如何做你想做的事,this blog 讨论了一个扩展方法WithCancellation,它可以让你停止等待你长时间运行的任务。

    【讨论】:

    • 本博客中的重要概念是您不能取消不支持取消的操作。您只能停止等待响应。在某些情况下,您可能能够中止任何需要很长时间的操作,例如通过中止线程或关闭数据库连接。在其他情况下,您不能,例如,您可以中止 REST 或 Web 服务请求。您只能停止等待服务器响应。服务器虽然不会知道你已经停止等待
    【解决方案2】:

    当您在button2_Click 上编写类似await Task.Run(() =&gt; { UpdateUI(() =&gt; { button2.Text = "Processing..."; }); }); 的内容时,您正在从 UI 线程调度一个操作到一个线程轮询线程,该线程轮询线程将一个操作发布到 UI 线程。如果您直接调用该操作,它会更快,因为它不会有两个上下文切换。

    ConfigureAwait(false) 导致同步上下文未被捕获。我不应该在 UI 方法中使用,因为你肯定想在 continuation 上做一些 UI 工作。

    你不应该使用Task.Factory.StartNew 代替Task.Run,除非你绝对有理由这样做。请参阅thisthis

    对于进度更新,请考虑使用Progress<T> class,因为它会捕获同步上下文。

    也许你应该尝试这样的事情:

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

    【讨论】:

    • 我前段时间读过 S.Toub 的文章,但最近找不到再读。所以,不得不做一个随机尝试错误的方法......这不是一个好的学习方法。我会试试你的代码。
    【解决方案3】:

    我同意 Paulo 回答中的所有观点 - 即,使用现代解决方案(Task.Run 而不是 Task.Factory.StartNewProgress&lt;T&gt; 进行进度更新,而不是手动发布到 SynchronizationContextTask.WhenAny 而不是 @ 987654326@ 用于异步代码)。

    但要回答实际问题:

    当我运行 CancellationSource.Cancel() 时,长时间运行的任务会继续工作并且不会取消。超时后,我对长时间运行的任务或它可能导致的任何结果不感兴趣。

    这有两个部分:

    • 如何编写响应取消请求的代码?
    • 如何编写在取消后忽略任何响应的代码?

    请注意,第一部分处理取消操作,第二部分实际上处理取消等待操作完成.

    第一件事:支持操作本身的取消。对于 CPU 绑定代码(即运行循环),定期调用 token.ThrowIfCancellationRequested()。对于 I/O 绑定代码,最好的选择是将 token 传递到下一个 API 层 - 大多数(但不是全部)I/O API 可以(应该)获取取消令牌。如果这不是一个选项,那么您可以选择忽略取消,也可以使用token.Register 注册取消回调。有时,您可以从 Register 回调中调用单独的取消方法,有时您可以通过从回调中释放对象来使其工作(这种方法通常有效,因为取消所有 I/O 的长期 Win32 API 传统手柄关闭时的手柄)。不过,我不确定这是否适用于 SqlConnection.Open

    接下来,取消等待。如果您只想取消等待由于超时,这个相对简单:

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

    【讨论】:

    • await Task.WhenAny(tWork, tProgress, Task.Delay(5000)); 如此简单直接!
    • @SergeMisnik:是的。如果您想实际观察一个真正的取消令牌,它会变得相当复杂,但如果它只是一个超时,那么它很简单。 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多