簡體   English   中英

如何在 TPL 中處理任務取消

[英]How to handle task cancellation in the TPL

再會! 我正在為 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... 有沒有辦法讓取消工作,或者甚至可能是一種不同的方法來編碼這些東西?

更新:

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

通過修改for (var i = 0; i < 25; i++) limit of i我模擬長時間運行的任務是否在進度任務之前完成或以其他方式完成。 根據需要工作。 WithCancellation輔助方法可以完成這項工作,盡管兩種“嵌套”的Task.WhenAny現在看起來很可疑。

我同意 Paulo 回答中的所有要點 - 即使用現代解決方案( Task.Run而不是Task.Factory.StartNewProgress<T>進行進度更新而不是手動發布到SynchronizationContextTask.WhenAny而不是Task.WaitAny對於異步代碼)。

但要回答實際問題:

當我運行 CancellationSource.Cancel() 時,長時間運行的任務會繼續工作並且不會取消。 超時后,我對長時間運行的任務或任何可能導致的結果不感興趣。

這有兩個部分:

  • 如何編寫響應取消請求的代碼?
  • 如何編寫在取消后忽略任何響應的代碼?

請注意,第一部分處理取消操作,第二部分實際上處理取消等待操作完成

第一件事:支持操作本身的取消。 對於 CPU 綁定代碼(即運行循環),定期調用token.ThrowIfCancellationRequested() 對於 I/O 綁定代碼,最好的選擇是將token向下傳遞到下一個 API 層 - 大多數(但不是全部)I/O API 可以(應該)獲取取消令牌。 如果這不是一個選項,那么您可以選擇忽略取消,或者您可以使用token.Register注冊取消回調。 有時,您可以從Register回調中調用一個單獨的取消方法,有時您可以通過從回調中處理對象來使其工作(這種方法通常有效,因為長期存在的 Win32 API 傳統是取消所有 I/O手柄關閉時的手柄)。 不過,我不確定這是否適用於SqlConnection.Open

接下來,取消等待 如果您只是想因超時而取消等待,則此方法相對簡單:

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

當你寫類似await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); }); button2_Click ,您從 UI 線程將操作調度到線程輪詢線程,該線程輪詢線程將操作發布到 UI 線程。 如果您直接調用操作,它會更快,因為它不會有兩個上下文切換。

ConfigureAwait(false)導致未捕獲同步上下文。 我不應該在 UI 方法中使用,因為你肯定想在延續上做一些 UI 工作。

除非絕對有理由,否則不應使用Task.Factory.StartNew而不是Task.Run 看到這個這個

對於進度更新,請考慮使用Progress<T> 類,因為它捕獲同步上下文。

也許你應該嘗試這樣的事情:

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

我認為您需要在progressUpdate操作中檢查IsCancellationRequested

至於如何做你想做的事, 這篇博客討論了一個擴展方法WithCancellation ,它可以讓你停止等待長時間運行的任務。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM