通常,我有希望能够超时的异步函数,这个超时与应用程序的更大范围无关。然而,函数还应该考虑更大的作用域,因此需要接收CancellationToken,它将指示应用程序作用域或其他更高级别作用域何时取消。
我还没有找到一种简单/标准的方法来处理这种情况,所以我决定编写一个小的扩展方法来创建一个新的CancellationToken,它将在最初的取消之后取消,但在超时之后也会取消。
我创建了以下内容,以便能够轻松、简洁地向接受取消令牌的任何任务添加带有超时的令牌:
public static CancellationToken NewWithTimeout(this CancellationToken original, int millisecondTimeout)
{
// EDIT: From comment, this part can be condensed into:
var src = CancellationTokenSource.CreateLinkedTokenSource(original);
src.CancelAfter(millisecondTimeout);
// CancellationTokenSource src = new(millisecondTimeout);
// original.Register(src.Cancel);
return src.Token;
}
public static CancellationToken NewWithTimeout(this CancellationToken original, TimeSpan timeout)
{
// EDIT: From comment, this part can be condensed into:
var src = CancellationTokenSource.CreateLinkedTokenSource(original);
src.CancelAfter(timeout);
// CancellationTokenSource src = new(timeout);
// original.Register(src.Cancel);
return src.Token;
}
然后,我还添加了以下内容,以便于创建可以超时的方法。这样,该方法将显式地显示它期望超时,并且它将在内部访问传入的要在内部使用的超时。
public class CancellationTokenWithTimeout
{
public CancellationToken Original { get; set; }
public CancellationToken Combined { get; set; }
public TimeSpan Timeout { get; set; }
public CancellationTokenWithTimeout(int millisecondTimeout, CancellationToken original)
{
CancellationTokenSource src = new(millisecondTimeout);
original.Register(src.Cancel);
Original = original;
Combined = src.Token;
Timeout = TimeSpan.FromMilliseconds(millisecondTimeout);
}
public CancellationTokenWithTimeout(TimeSpan timeout, CancellationToken original)
{
CancellationTokenSource src = new(timeout);
original.Register(src.Cancel);
Original = original;
Combined = src.Token;
Timeout = timeout;
}
}
public static class ExtensionMethods
{
public static CancellationTokenWithTimeout AddTimeout(this CancellationToken original, int millisecondTimeout)
{
return new CancellationTokenWithTimeout(millisecondTimeout, original);
}
public static CancellationTokenWithTimeout AddTimeout(this CancellationToken original, TimeSpan timeout)
{
return new CancellationTokenWithTimeout(timeout, original);
}
public static CancellationToken NewWithTimeout(this CancellationToken original, int millisecondTimeout)
{
CancellationTokenSource src = new(millisecondTimeout);
original.Register(src.Cancel);
return src.Token;
}
public static CancellationToken NewWithTimeout(this CancellationToken original, TimeSpan timeout)
{
CancellationTokenSource src = new(timeout);
original.Register(src.Cancel);
return src.Token;
}
}
使用此实现,您现在可以执行以下操作:
public class SomeWorker : BackgroundService
{
private readonly ILogger<SomeWorker> _logger;
public SomeWorker(ILogger<SomeWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("SomeWorker running at: {time}", DateTimeOffset.Now);
try
{
var longTask = DoSomethingAsync("Hello", stoppingToken.NewWithTimeout(2000));
await Task.Delay(1000, stoppingToken);
_logger.LogInformation("One second has passed!");
await longTask;
_logger.LogInformation("Long task is done.");
}catch (OperationCanceledException ex)
{
if (stoppingToken.IsCancellationRequested)
_logger.LogInformation("We got cancelled!");
else
_logger.LogInformation("Task timed out!");
}
_logger.LogInformation("Another Test with explicit timeout!");
try
{
var longTask = DoSomethingAsyncWithTimeout("Hello", stoppingToken.AddTimeout(2000));
await Task.Delay(1000, stoppingToken);
_logger.LogInformation("One second has passed!");
await longTask;
_logger.LogInformation("Long task is done.");
}
catch (OperationCanceledException ex)
{
if (stoppingToken.IsCancellationRequested)
_logger.LogInformation("We got cancelled!");
else
_logger.LogInformation("Task timed out! This must have come from the inner task...");
}
catch (TimeoutException timeout)
{
_logger.LogInformation("Task timed out! Either from the task or the inner task");
}
}
}
public async Task<string> DoSomethingAsync(string param1, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
(bool done, string result) = await SomeOtherAsync(cancellationToken.Combined);
if (done) return result;
}
throw new TaskCanceledException();
}
public async Task<string> DoSomethingAsyncWithTimeout(string param1, CancellationTokenWithTimeout cancellationToken)
{
while (!cancellationToken.Combined.IsCancellationRequested)
{
(bool _, string intermediateresult) = await SomeOtherAsync(cancellationToken.Combined);
(bool done, string result) = await SomeOtherAsyncWithTimeout(intermediateresult, cancellationToken.Original, cancellationToken.Timeout);
if (done) return result;
}
if (cancellationToken.Original.IsCancellationRequested)
throw new TaskCanceledException();
else
throw new TimeoutException();
}
}
我想知道这个实现是否有任何问题,以及是否有一种类似的简洁的方法,不需要任何自定义扩展。如果这样做是明智的话,也不要做任何改进。
发布于 2022-03-25 11:04:09
有一个名为.NET的波莉库,它定义了几个通用的回弹力策略。最著名的是超时,重试和电路断路器.
这些政策被用作装饰者。定义策略的行为方式,然后将其应用于任意代码。同步和异步代码的处理方式不同,因此在定义策略时必须注意到这一点。
超时策略可以在两种模式下工作:乐观和悲观。前者允许您通过用户提供的CancellationToken
或超时策略本身取消修饰方法。
public IAsyncPolicy CreateTimeoutConstraint(TimeSpan threshold)
=> Policy.TimeoutAsync(threshold, TimeoutStrategy.Optimistic);
public async Task ExecuteMethodWithTimeConstraintAsync(CancellationToken userProvideToken)
{
var timeoutPolicy = CreateTimeoutConstraint(TimeSpan.FromSeconds(2));
await timeoutPolicy.ExecuteAsync(async (ct) => await SomeBackgroundTask(ct), userProvideToken);
}
发布于 2022-03-17 19:41:04
如果创建扩展方法应该返回CancellationTokenSource
,则可以将其释放。见StackOverflow问题"什么时候处置总理府?“
我通常如何向现有令牌添加超时的示例。
// This token will cancell when timeelaspes or the stoppingToken is cancelled
using var timeOutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
timeOutTokenSource.CancelAfter(TimeSpan.FromSeconds(2));
// Pass the linked Token down into calling method
await DoSomethingAsync("hello", timeOutTokenSource.Token);
至于CancellationTokenWithTimeout
,我会让它使用构造函数中的扩展方法来生成一个新的CancellationTokenTokenSource
,也可以使用IDisposable
来释放资源。当我需要像CancellationTokenWithTimeout
这样的类时,我个人从来没有遇到过这样的情况,因为通常只有一个令牌被传递给基于其他令牌的取消,这样就足够了。
如果我希望有一个超时,我们可以完成相同的事情,而不必创建一个新的令牌
// store task we want to wait
var doSomethingTask = DoSomethingAsync("hello", stoppingToken);
// Make a task that is Delayed based on timeout and return when either finish
await Task.WhenAny(Task.Delay(TimeSpan.FromSeconds(2), stoppingToken), doSomethingTask);
if (!doSomethingTask.IsCompleted)
{
// We are here because Task.Delay completed this is out timeout code
}
您仍然希望创建一个链接并传递的CancellationTokenSource
,即使使用Task.Delay
,您也可以在超时代码中取消令牌。
https://codereview.stackexchange.com/questions/275018
复制相似问题