簡體   English   中英

取消請求向所有任務的傳播時間(TPL)

[英]Propagation time of cancellation request to all tasks (TPL)

使用TPL,我們有CancellationTokenSource ,它提供令牌,可用於以協作方式取消當前任務(或其開始)。

題:

將取消請求傳播到所有已掛起的正在運行的任務需要多長時間? 有什么地方的代碼可以用來檢查以下內容:從現在開始,每一個感興趣的Task都會發現已請求取消嗎?


為什么需要它?

我想進行穩定的單元測試,以表明取消在我們的代碼中有效。

問題詳細信息:

我們有產生任務的“執行程序”,這些任務包裝了一些長時間運行的動作。 執行程序的主要工作是限制啟動了多少個並發動作。 所有這些任務都可以單獨取消,並且這些操作也將在內部遵守CancellationToken

我想提供單元測試,該測試表明當任務等待插槽開始給定操作時發生取消操作時 ,該任務將自行取消(最終),並且不會開始執行給定操作

因此,想法是用單個插槽准備LimitingExecutor 然后開始阻止操作 ,該操作將在取消阻止時請求取消。 然后“排隊” 測試操作 ,該操作應在執行時失敗。 使用該設置,測試將調用unblock ,然后斷言測試操作的任務將在等待時拋出TaskCanceledException

[Test]
public void RequestPropagationTest()
{
    using (var setupEvent = new ManualResetEvent(initialState: false))
    using (var cancellation = new CancellationTokenSource())
    using (var executor = new LimitingExecutor())
    {
        // System-state setup action:
        var cancellingTask = executor.Do(() =>
        {
            setupEvent.WaitOne();
            cancellation.Cancel();
        }, CancellationToken.None);

        // Main work action:
        var actionTask = executor.Do(() =>
        {
            throw new InvalidOperationException(
                "This action should be cancelled!");
        }, cancellation.Token);

        // Let's wait until this `Task` starts, so it will got opportunity
        // to cancel itself, and expected later exception will not come
        // from just starting that action by `Task.Run` with token:
        while (actionTask.Status < TaskStatus.Running)
            Thread.Sleep(millisecondsTimeout: 1);

        // Let's unblock slot in Executor for the 'main work action'
        // by finalizing the 'system-state setup action' which will
        // finally request "global" cancellation:
        setupEvent.Set();

        Assert.DoesNotThrowAsync(
            async () => await cancellingTask);

        Assert.ThrowsAsync<TaskCanceledException>(
            async () => await actionTask);
    }
}

public class LimitingExecutor : IDisposable
{
    private const int UpperLimit = 1;
    private readonly Semaphore _semaphore
        = new Semaphore(UpperLimit, UpperLimit);

    public Task Do(Action work, CancellationToken token)
        => Task.Run(() =>
        {
            _semaphore.WaitOne();
            try
            {
                token.ThrowIfCancellationRequested();
                work();
            }
            finally
            {
                _semaphore.Release();
            }
        }, token);

    public void Dispose()
        => _semaphore.Dispose();
}

這個問題的可執行演示(通過NUnit)可以在GitHub上找到

但是,該測試的實現有時會失敗(沒有預期的TaskCanceledException ),在我的機器上可能有十分之一的運行。 解決該問題的一種方法是在取消請求之后立即插入Thread.Sleep 即使睡眠3秒鍾,該測試有時仍會失敗(運行20次后才能發現),並且通過測試后,通常無需長時間等待(我想)。 作為參考,請參見diff

“其他問題”,是為了確保取消來自“等待時間”,而不是來自Task.Run ,因為ThreadPool可能很忙(其他正在執行測試),並且它在取消請求后將第二個任務的啟動推遲-這將導致此測試為“偽綠色”。 “通過黑客輕松解決”是主動等待直到第二個任務開始-其Status變為TaskStatus.Running 請檢查此分支下的版本,看看沒有此hack的測試有時會是“綠色”-因此示例錯誤可能會通過。

您的測試方法假定cancellingTask始終在actionTask之前actionTask LimitingExecutor的插槽(輸入信號量)。 不幸的是,這個假設是錯誤的, LimitingExecutor不能保證這一點,這只是運氣,這兩個任務中的哪一個占用了插槽(實際上在我的計算機上,它僅發生在5%的運行次數中)。

要解決此問題,您需要另一個ManualResetEvent ,它將允許主線程等待,直到cancellingTask實際占用插槽為止:

using (var slotTaken = new ManualResetEvent(initialState: false))
using (var setupEvent = new ManualResetEvent(initialState: false))
using (var cancellation = new CancellationTokenSource())
using (var executor = new LimitingExecutor())
{
    // System-state setup action:
    var cancellingTask = executor.Do(() =>
    {
        // This is called from inside the semaphore, so it's
        // certain that this task occupies the only available slot.
        slotTaken.Set();

        setupEvent.WaitOne();
        cancellation.Cancel();
    }, CancellationToken.None);

    // Wait until cancellingTask takes the slot
    slotTaken.WaitOne();

    // Now it's guaranteed that cancellingTask takes the slot, not the actionTask

    // ...
}


.NET Framework不提供用於檢測任務向Running狀態轉換的API,因此,如果您不喜歡在循環中輪詢State屬性+ Thread.Sleep() ,則需要將LimitingExecutor.Do()修改為提供此信息,可能使用另一個ManualResetEvent ,例如:

public Task Do(Action work, CancellationToken token, ManualResetEvent taskRunEvent = null)
    => Task.Run(() =>
    {
        // Optional notification to the caller that task is now running
        taskRunEvent?.Set();

        // ...
    }, token);

暫無
暫無

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

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