Вопрос или проблема
Я пытался разобраться, почему я получаю TaskCanceledException для небольшого асинхронного кода, который в последнее время стал вести себя неправильно. Я уменьшил свою проблему до небольшого фрагмента кода, который заставляет меня чесать затылок:
static void Main(string[] args)
{
RunTest();
}
private static void RunTest()
{
Task.Delay(1000).ContinueWith(t => Console.WriteLine("{0}", t.Exception), TaskContinuationOptions.OnlyOnFaulted).Wait();
}
Насколько я знаю, это просто должно приостановить выполнение на секунду, а затем закрыться. ContinueWith не должен вызываться (это относится только к моему реальному случаю использования). Однако вместо этого я получаю TaskCanceledException, и понятия не имею, откуда это берётся!
Я также получил эту ошибку:
Фрагмент кода выглядел так:
private void CallMediator<TRequest>(TRequest request) where TRequest : IRequest<Unit>
{
_ = Task.Run(async () =>
{
var mediator = _serviceScopeFactory.CreateScope().ServiceProvider.GetService<IMediator>()!;
await mediator.Send(request).ContinueWith(LogException, TaskContinuationOptions.OnlyOnFaulted);
});
}
private void LogException(Task task)
{
if (task.Exception != null)
{
_logger.LogError(task.Exception, "{ErrorMessage}", task.Exception.Message);
}
}
Читая документацию к методу ContinueWith, там есть следующие замечания:
Возвращённая задача не будет запланирована на выполнение, пока текущая задача не будет завершена. Если критерии продолжения, указанные через параметр
continuationOptions
, не выполнены, задача продолжения будет отменена, а не запланирована.
Таким образом, для меня сначала была вызвана первая задача (mediator.Send(request)
), затем продолжилась задача ContinueWith(...)
, которую я await
ил. Однако, поскольку в первой задаче исключение не произошло, вторая задача была отменена. Поэтому, ожидая вторую задачу, я получил TaskCanceledException
.
Что я сделал, так это изменил код на этот:
private void CallMediator<TRequest>(TRequest request) where TRequest : IRequest<Unit>
{
_ = Task.Run(async () =>
{
var mediator = _serviceScopeFactory.CreateScope().ServiceProvider.GetService<IMediator>()!;
try
{
_ = await mediator.Send(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "{ErrorMessage}", ex.Message);
}
});
}
Вместо использования .ContinueWith(...)
я заменил его просто на обычный блок try-catch на случай, если задача, которая меня интересует, завершится с ошибкой. Думаю, это упрощает код и делает его более читаемым.
В вопросе есть такая строка кода:
Task.Delay(1000).ContinueWith(t => Console.WriteLine("{0}", t.Exception), TaskContinuationOptions.OnlyOnFaulted).Wait();
Я бы переписал это на:
try
{
Task.Delay(1000).Wait();
}
catch (Exception ex)
{
Console.WriteLine("{0}", ex);
}
Вы используете неверный параметр taskcontinuationoption:
Смотрите следующую ссылку: https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions%28v=vs.110%29.aspx
Там говорится:
Указывает, что задача продолжения должна быть запланирована только в том случае, если предшествующая задача выбросила необработанное исключение. Этот параметр неvalid для многозадачных продолжений.
Как ребята сказали выше, этот вызов требует, чтобы предшествующая задача была в состоянии ошибки, иначе возникнет TaskCanceledException. Для этого конкретного случая вы можете обобщить ContinueWith, чтобы обрабатывать все статусы:
await Task.Delay(1000).ContinueWith(
task =>
{
/* учитывайте, что отмененная задача выбрасывает в следующей строке TaskCancelledException */
if (!task.IsFaulted) {
return;
}
Console.WriteLine("{0}", task.Exception);
// делаем что-то вроде 'throw task.Exception.InnerException'
});
Ответ или решение
Если вы сталкиваетесь с исключением TaskCanceledException
при использовании ContinueWith
в асинхронном коде, это может быть связано с тем, что параметр TaskContinuationOptions.OnlyOnFaulted
вызывает отмену задачи продолжения, если заранее определенная (antecedent) задача завершилась успешно или была отменена.
Рассмотрим ваш код подробнее:
private static void RunTest()
{
Task.Delay(1000).ContinueWith(t => Console.WriteLine("{0}", t.Exception), TaskContinuationOptions.OnlyOnFaulted).Wait();
}
В данном случае, Task.Delay(1000)
– это задача, которая должна завершиться успешно через 1 секунду. Однако, когда используется ContinueWith
с опцией OnlyOnFaulted
, дальнейшие действия не будут выполнены, если задача не завершилась с ошибкой. Это означает, что в момент, когда ContinueWith
проверяет состояние задачи, она завершилась успешно, и как следствие, задача продолжения отменяется. Именно на этом этапе возникает TaskCanceledException
.
Чтобы избежать этой проблемы, вы можете изменить проход кода следующим образом:
- Изменить способ обработки результата асинхронной задачи.
- Использовать обычный блок
try-catch
для обработки ошибок вместоContinueWith
.
Вот как может выглядеть исправленный код:
private static void RunTest()
{
try
{
Task.Delay(1000).Wait(); // Ждем 1 секунду
}
catch (Exception ex)
{
Console.WriteLine("{0}", ex);
}
}
Это подход избавляет вас от необходимости беспокоиться об исключениях, возникающих в контексте ContinueWith
и делает код более читабельным.
Что касается более сложного случая с вызовом mediator.Send(request)
, вы вполне правильно подошли к решению с использованием try-catch
:
private void CallMediator<TRequest>(TRequest request) where TRequest : IRequest<Unit>
{
_ = Task.Run(async () =>
{
var mediator = _serviceScopeFactory.CreateScope().ServiceProvider.GetService<IMediator>()!;
try
{
await mediator.Send(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "{ErrorMessage}", ex.Message);
}
});
}
Таким образом, вы обрабатываете исключения прямо в блоке try-catch
, что является подходом более универсальным и безопасным в случаях, когда исключения могут быть выброшены.
Таким образом, рекомендуется не полагаться на ContinueWith
с OnlyOnFaulted
, если вы не уверены, что задача всегда завершится ошибкой, так как это может привести к неочевидным результатам и сложностям в обработке исключений.