[英]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#4和Gotcha#5的組合
整篇文章(以及發布到這個問題的答案)確實提高了我的整體理解。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.