簡體   English   中英

C#任務忽略取消超時

[英]C# Task Ignoring Cancellation timeout

我正在嘗試為任意代碼編寫一個包裝器,它將在給定的超時時間段之后取消(或至少停止等待)代碼。

我有以下測試和實現

[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
    var cts = new CancellationTokenSource();
    var fakeService = new Mock<IFakeService>();
    IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
    Assert.Throws<TimeoutException>(async () => await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token));

    fakeService.Verify(f=>f.DoStuff(),Times.Never);
}

和“DoStuff”方法

private static async Task DoStuff(int sleepTime, IFakeService fakeService)
{

    await Task.Delay(sleepTime).ConfigureAwait(false);
    var result = await Task.FromResult("bob");
    var test = result + "test";
    fakeService.DoStuff();
}

並執行IExecutionPolicy.ExecuteAsync

public async Task ExecuteAsync(Action action, CancellationToken token)
{
    var cts = new CancellationTokenSource();//TODO: resolve ignoring the token we were given!

    var task = _decoratedPolicy.ExecuteAsync(action, cts.Token);
    cts.CancelAfter(_timeout);

    try
    {
        await task.ConfigureAwait(false);
    }
    catch(OperationCanceledException err)
    {
        throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms", err);
    }
}

應該發生的是,測試方法嘗試采取> 3000ms並且超時應該發生在20ms,但這不會發生。 為什么我的代碼沒有按預期超時?

編輯:

根據要求 - decoratedPolicy如下

public async Task ExecuteAsync(Action action, CancellationToken token)
{
    token.ThrowIfCancellationRequested();
    await Task.Factory.StartNew(action.Invoke, token);  
}

如果我理解正確,那么您正嘗試支持不支持超時/取消的方法的超時。

通常,這是通過啟動具有所需超時值的計時器來完成的。 如果計時器首先觸發,那么您可以拋出異常。 使用TPL,您可以使用Task.Delay(_timeout)而不是計時器。

public async Task ExecuteAsync(Action action, CancellationToken token)
{
    var task = _decoratedPolicy.ExecuteAsync(action, token);

    var completed = await Task.WhenAny(task, Task.Delay(_timeout));
    if (completed != task)
    {
        throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
    }
}

注意:這不會停止執行_decoratedPolicy.ExecuteAsync方法,而是忽略它。

如果您的方法支持取消(但不及時),則最好在超時后取消任務。 您可以通過創建鏈接令牌來完成。

public async Task ExecuteAsync(Action action, CancellationToken token)
{
    using(var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token))
    {
        var task = _decoratedPolicy.ExecuteAsync(action, linkedTokenSource.Token);

        var completed = await Task.WhenAny(task, Task.Delay(_timeout));
        if (completed != task)
        {
            linkedTokenSource.Cancel();//Try to cancel the method
            throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
        }
    }
}

使用CancellationToken意味着您正在進行合作取消。 設置CancellationTokenSource.CancelAfter將在指定的時間后將基礎標記轉換為已取消狀態,但如果該標記未被調用異步方法監視 ,則不會發生任何事情。

為了讓這實際上產生OperationCanceledException ,你需要調用cts.Token.ThrowIfCancellationRequested_decoratedPolicy.ExecuteAsync

例如:

// Assuming this is _decoratedPolicy.ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
     // This is what creates and throws the OperationCanceledException
     token.ThrowIfCancellationRequested();

     // Simulate some work
     await Task.Delay(20);
}

編輯:

為了實際取消令牌,您需要在執行工作的所有點監視它,並且執行可能會超時。 如果你不能做出這種保證,那么請遵循@SriramSakthivel回答實際Task被丟棄的地方,而不是實際取消。

您正在調用Assert.Throws(Action action),並且您的匿名異步方法將轉換為async void。 該方法將與Fire&Forget語義異步調用,而不會拋出異常。

但是,由於異步void方法中的未捕獲異常,該過程很快就會崩潰。

您應該同步調用ExecuteAsync:

[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
    var cts = new CancellationTokenSource();
    var fakeService = new Mock<IFakeService>();
    IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
    Assert.Throws<AggregateException>(() => policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token).Wait());

    fakeService.Verify(f=>f.DoStuff(),Times.Never);
}

或使用異步測試方法:

[Test]
public async Task Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
    var cts = new CancellationTokenSource();
    var fakeService = new Mock<IFakeService>();
    IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());

    try
    {
        await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token);
        Assert.Fail("Method did not timeout.");
    }
    catch (TimeoutException)
    { }

    fakeService.Verify(f=>f.DoStuff(),Times.Never);
}

我決定在這里回答我自己的問題,因為雖然每個列出的答案都解決了我需要做的事情,但他們沒有確定這個問題的根本原因。 很多,非常感謝:Scott Chamberlain,Yuval Itzchakov,Sriram Sakthivel,Jeff Cyr。 感謝所有的建議。

根本原因/解決方案:

await Task.Factory.StartNew(action.Invoke, token);

您在我的“裝飾策略”中看到的內容返回一個任務,並等待外部任務等待。 用它替換它

await Task.Run(async () => await action.Invoke());

得到正確的結果。

我的代碼遭受了來自C#async陷阱的優秀文章的Gotcha#4Gotcha#5的組合

整篇文章(以及發布到這個問題的答案)確實提高了我的整體理解。

暫無
暫無

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

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