Почему мой процесс не завершается, когда Task имеет необработанное исключение?

Я создаю службу Windows с .NET 4.0.

У меня есть различные необработанные исключения, заданные в Задачи, но они не завершают мой процесс, поскольку состояния документации MSDN (Parallel Tasks - см. Unobserved Task Исключения).

"Если вы не дадите ошибочной задаче возможность распространения ее исключения (например, вызывая метод Wait), время выполнения эскалировать задачу незаметными исключениями в соответствии с текущими .NET, когда задача собирает мусор."

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

Task.Factory.StartNew(() => { throw new Exception(); } 

Служба продолжает работать нормально, когда она вызывается.

В соответствии с документами финализатор Задачи будет отменять исключение, если Задача GC'd, но это, похоже, не происходит. MSDN неоднократно заявляет, что нормальная "политика исключений .NET" приводит к завершению процесса.

Почему это не прекращает мое приложение? Единственное, что я могу подумать, - это как-то ссылка на заданную задачу (это лямбда?)

Ответ 1

От Essential С# 4.0, страница 715, может помочь следующее:

Необработанное исключение во время выполнения задачи будет подавлено до вызова одного из членов завершения задачи: Wait(), Result,Task.WaitAll() или Task.WaitAny(). Каждый из этих членов будет бросить любые необработанные исключения, которые произошли в рамках задачи выполнение.

Доступны несколько способов обработки исключений. посмотрите здесь MSDN.

В ответ на ваш комментарий другая цитата из той же книги объясняет, почему некоторые исключения не распространяются:

Хотя относительно редко, одно из исключений для общего правила (барботажа) происходит в Задаче. [..] Любые исключения на основе задач выбрасывается из очереди финализации во время выхода приложения подавлено. Поведение настроено таким образом, потому что часто эффект для обработки такого исключения он слишком сложный [...]

Чтобы преодолеть это, один из способов сделать это изящно состоит в том, чтобы создать задачу обработчика исключений и использовать ContinueWith для отслеживания после запуска вашей задачи. Затем вы можете использовать parentTask.IsFaulted и изящно сбой, даже в случае, если исключение выбрасывается в очередь финализации во время выхода приложения.

Совет. Используйте флаг OnlyOnFaulted, чтобы эта задача выполнялась только при возникновении исключения.

Ответ 2

Как было предложено @Hans и @CodeInChaos, я нашел единственный способ переустановить необработанное исключение (таким образом, убив процесс) - заставить Finalizer работать (Примечание). Убедитесь, что вы этого не делаете в ContinueWith()!):

GC.Collect(); 
GC.WaitForPendingFinalizers();

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

Интересно, что даже делать GC.Collect() недостаточно. Финализатор задачи все еще не запускался. GC.WaitForPendingFinalizers() нужно было называть явно. (Я подозреваю, что не понимаю тонкостей вокруг завершения).

Подводя итог: не ожидайте, что поведение исключаемых исключений TPL Task не будет аналогично другим механизмам обработки потоков без обработки (например, QueueUserWorkItem). В большинстве практических ситуаций вам нужно выполнить проверку на исключение из "Исключения из задач": вы не можете полагаться на незаметные исключения, которые будут доведены до вашего сведения так, как они будут с QUWI или похожими, потому что вы увидите только их брошенное из Finalizer, которое совершенно непредсказуемо.

Изменить: см. мой другой ответ в отношении .NET 4.5

Ответ 3

.NET 4.5 внесла некоторые изменения относительно как обрабатываются UnobservedExceptions

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

Такое поведение можно настроить, так что вы можете вернуться в режим .NET 4.0, включив ThrowUnobservedTaskExceptions следующим образом:

<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime>
</configuration>

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

Ответ 4

Вы можете создать его с помощью TaskCreationOptions.AttachedToParent. Согласно Вложенные задачи и дочерние задачи (MSDN), Исключения затем распространяются в ваш поток. Однако я не знаю, элегантно это или нет.

Microsoft не рекомендует это "в большинстве случаев". Кто-то другой может знать, в каком случае это может быть разумным. Из той же статьи:

Вы можете использовать связанные дочерние задачи для создания тесно синхронизированного графики асинхронных операций. Однако в большинстве сценариев мы рекомендуется использовать вложенные задачи, поскольку отношения с другие задачи менее сложны. Вот почему задачи, созданные внутри других задачи вложены в по умолчанию, и вы должны явно указать AttachedToParent для создания дочерней задачи.

Приветствия, Маттиас

Ответ 5

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

public static Task FailFastOnException(this Task task) 
{ 
    task.ContinueWith(c => Environment.FailFast("Task faulted", c.Exception), 
    TaskContinuationOptions.OnlyOnFaulted | 
    TaskContinuationOptions.ExecuteSynchronously | 
    TaskContinuationOptions.DetachedFromParent); 
    return task; 
}