首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >c#中带有超时的异步任务的简洁处理

c#中带有超时的异步任务的简洁处理
EN

Code Review用户
提问于 2022-03-17 14:48:05
回答 2查看 1.4K关注 0票数 2

通常,我有希望能够超时的异步函数,这个超时与应用程序的更大范围无关。然而,函数还应该考虑更大的作用域,因此需要接收CancellationToken,它将指示应用程序作用域或其他更高级别作用域何时取消。

我还没有找到一种简单/标准的方法来处理这种情况,所以我决定编写一个小的扩展方法来创建一个新的CancellationToken,它将在最初的取消之后取消,但在超时之后也会取消。

我创建了以下内容,以便能够轻松、简洁地向接受取消令牌的任何任务添加带有超时的令牌:

代码语言:javascript
运行
复制
        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;
        }

然后,我还添加了以下内容,以便于创建可以超时的方法。这样,该方法将显式地显示它期望超时,并且它将在内部访问传入的要在内部使用的超时。

代码语言:javascript
运行
复制
   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;
        }
    }

使用此实现,您现在可以执行以下操作:

代码语言:javascript
运行
复制
    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();
        }

    }

我想知道这个实现是否有任何问题,以及是否有一种类似的简洁的方法,不需要任何自定义扩展。如果这样做是明智的话,也不要做任何改进。

EN

回答 2

Code Review用户

回答已采纳

发布于 2022-03-25 11:04:09

有一个名为.NET的波莉库,它定义了几个通用的回弹力策略。最著名的是超时重试电路断路器.

这些政策被用作装饰者。定义策略的行为方式,然后将其应用于任意代码。同步和异步代码的处理方式不同,因此在定义策略时必须注意到这一点。

超时策略可以在两种模式下工作:乐观和悲观。前者允许您通过用户提供的CancellationToken或超时策略本身取消修饰方法。

代码语言:javascript
运行
复制
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);
}
票数 1
EN

Code Review用户

发布于 2022-03-17 19:41:04

如果创建扩展方法应该返回CancellationTokenSource,则可以将其释放。见StackOverflow问题"什么时候处置总理府?

我通常如何向现有令牌添加超时的示例。

代码语言:javascript
运行
复制
// 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这样的类时,我个人从来没有遇到过这样的情况,因为通常只有一个令牌被传递给基于其他令牌的取消,这样就足够了。

如果我希望有一个超时,我们可以完成相同的事情,而不必创建一个新的令牌

代码语言:javascript
运行
复制
// 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,您也可以在超时代码中取消令牌。

票数 0
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/275018

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档