Использование Task.Delay в Task.Run - PullRequest
0 голосов
/ 20 апреля 2020

У меня есть 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);
}

Есть ли очевидные проблемы с этим кодом?

Ответы [ 2 ]

3 голосов
/ 20 апреля 2020

Есть ли какие-либо очевидные проблемы с этим кодом?

То, что мне выпадает, - это подсчет количества миллисекунд для задержки. Конкретно нет пола. Если action() занимает необычайно много времени, то задача может завершиться неудачей, возможно, неожиданным образом.

Есть несколько способов выполнить задачу в отмененном или неудачном состоянии, или она может задержаться навсегда:

  1. Задача может быть отменена до начала делегата, поскольку токен отмены передан Task.Run.
  2. Задача может быть отменена вызовом ThrowIfCancellationRequested.
  3. Задание может быть успешно выполнено после отмены из-за IsCancellationRequested logi c.
  4. Задача может быть отменена с помощью токена отмены, переданного Task.Delay.
  5. Задача может завершиться с ArgumentOutOfRangeException, если _settings.loopStepFrequency - sw.ElapsedMilliseconds меньше -1. Вероятно, это ошибка.
  6. Задание может задержаться на неопределенное время (до отмены), если _settings.loopStepFrequency - sw.ElapsedMilliseconds окажется точно -1. Вероятно, это ошибка.

Чтобы исправить этот код, я рекомендую две вещи:

  1. Код, вероятно, намеревается сделать await Task.Delay((int) wait, ct); вместо await Task.Delay((int)(_settings.loopStepFrequency - sw.ElapsedMilliseconds), ct);. Это удалит последние два условия выше.
  2. Выберите один из методов отмены. Стандартный шаблон для отмены express - через OperationCanceledExcpetion; это шаблон, используемый ThrowIfCancellationRequested и Task.Delay. Проверка IsCancellationRequested использует другой шаблон; он успешно выполнит задачу отмены, а не отменяет ее.
1 голос
/ 20 апреля 2020

В этом коде так много проблем, что имеет смысл переписать его, чем пытаться исправить. Вот один из возможных способов переписать этот метод с добавлением некоторой (возможно, лишней) проверки аргументов:

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);
}
...