【问题标题】:Using Task.Delay within Task.Run在 Task.Run 中使用 Task.Delay
【发布时间】:2020-04-20 09:20:38
【问题描述】:

我有一个 Windows 服务,它通过每秒运行几个测试来监控我的应用程序。已经提交了一个错误报告,说服务在一段时间后停止运行,我正在尝试找出原因。

我怀疑下面的代码是罪魁祸首,但我无法准确理解它是如何工作的。 ContinueWith语句最近被注释掉了,不知道是不是需要

private Task CreateTask(Action action)
{
    var ct = _cts.Token;
    return Task.Run(async () =>
        {
            ct.ThrowIfCancellationRequested();
            var sw = new Stopwatch();
            while (true)
            {
                sw.Restart();
                action();

                if (ct.IsCancellationRequested)
                {
                    _logger.Debug("Cancellation requested");
                    break;
                }

                var wait = _settings.loopStepFrequency - sw.ElapsedMilliseconds;
                if (wait <= 0) // No need to delay
                    continue;

                // If ContinueWith is needed wrap this in an ugly try/catch 
                // handling the exception
                await Task.Delay(
                    (int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds),
                    ct); //.ContinueWith(tsk => { }, ct);
            }

            _logger.Debug("Task was cancelled");
        }, _cts.Token);
}

这段代码有什么明显的问题吗?

【问题讨论】:

  • 您可能希望使用System.Timers.Timer 来安排测试。如果您的 _ctsTask.Delay 中被取消,您将收到异常,因此您可能需要为此使用 try-catch。
  • 为什么您认为这与服务停止问题有关?

标签: c# async-await windows-services topshelf


【解决方案1】:

这段代码有什么明显的问题吗?

让我想到的是延迟毫秒数的计算。具体来说,没有地板。如果action() 花费了异常长的时间,那么任务可能会以一种可能出乎意料的方式失败。

有几种方法可以让任务在取消或失败状态下完成,或者它可以永远延迟:

  1. 可以在委托开始之前取消任务,因为取消令牌传递给Task.Run
  2. 可以通过ThrowIfCancellationRequested 调用取消任务。
  3. 由于IsCancellationRequested逻辑,任务被取消后可以成功完成。
  4. 可以通过传递给Task.Delay 的取消令牌取消任务。
  5. 如果_settings.loopStepFrequency - sw.ElapsedMilliseconds 小于-1,任务可能会失败并返回ArgumentOutOfRangeException。这可能是一个错误。
  6. 如果_settings.loopStepFrequency - sw.ElapsedMilliseconds 恰好是-1,任务可能会无限期延迟(直到取消)。这可能是一个错误。

要修复此代码,我建议做两件事:

  1. 代码可能打算执行await Task.Delay((int) wait, ct); 而不是await Task.Delay((int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds), ct);。这将删除上述最后两个条件。
  2. 选择一种取消方法。表示取消的标准模式是通过OperationCanceledExcpetion;这是ThrowIfCancellationRequestedTask.Delay 使用的模式。 IsCancellationRequested 检查使用不同的模式;它会在取消时成功完成任务,而不是取消它。

【讨论】:

  • 感谢您的详细解答!我有几个问题:如果 action() 花费了异常长的时间,那么 'var wait' 将变为负数并且不会发生延迟?
  • 关于第5点:只有在“var wait”设置为1或更大时才会发生这种情况,然后需要2ms才能到达Delay语句?
  • @Q-bertsuit:是的,是的。
【解决方案2】:

这段代码有很多问题,重写它比尝试修复它更有意义。这是重写此方法的一种可能方法,并添加了一些(可能是多余的)参数验证:

private Task CreateTask(Action action)
{
    if (action == null) throw new ArgumentNullException(nameof(action));
    var ct = _cts.Token;
    var delayMsec = _settings.loopStepFrequency;
    if (delayMsec <= 0) throw new ArgumentOutOfRangeException("loopStepFrequency");
    return Task.Run(async () =>
    {
        while (true)
        {
            var delayTask = Task.Delay(delayMsec, ct);
            action();
            await delayTask;
        }
    }, ct);
}

记录可能的异常/取消的责任现在属于方法的调用者,它(希望)等待创建的任务。

var task = CreateTask(TheAction);
try
{
    await task; // If the caller is async
    //task.GetAwaiter().GetResult(); // If the caller is sync
    _logger.Info("The task completed successfully");
}
catch (OperationCanceledException)
{
    _logger.Info("The task was canceled");
}
catch (Exception ex)
{
    _logger.Error("The task failed", ex);
}

【讨论】:

  • 非常感谢您!非常感谢
猜你喜欢
  • 1970-01-01
  • 2019-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多