Как поймать/наблюдать необработанное исключение, брошенное из Задачи
Я пытаюсь регистрировать/сообщать обо всех необработанных исключениях в моем приложении (решение для отчетов об ошибках). Я столкнулся с сценарием, который всегда необработан. Мне интересно, как я поймаю эту ошибку необработанным образом. Обратите внимание, что сегодня утром я провел много исследований и много чего пытался. Да, я видел этот, и многое другое. Я просто ищу универсальное решение для регистрации необработанных исключений.
У меня есть следующий код внутри основного метода консольных тестовых приложений:
Task.Factory.StartNew(TryExecute);
или
Task.Run((Action)TryExecute);
а также следующий метод:
private static void TryExecute() {
throw new Exception("I'm never caught");
}
Я уже пробовал подключиться к следующему в своем приложении, но они никогда не вызываются.
AppDomain.CurrentDomain.UnhandledException
TaskScheduler.UnobservedTaskException
В моем приложении Wpf, где я изначально обнаружил эту ошибку, я также подключился к этим событиям, но он никогда не вызывался.
Dispatcher.UnhandledException
Application.Current.DispatcherUnhandledException
System.Windows.Forms.Application.ThreadException
Единственный обработчик, который вызывается когда-либо:
AppDomain.CurrentDomain.FirstChanceException
но это не является допустимым решением, так как я хочу только сообщать о неперехваченных исключениях (не каждое исключение, как FirstChanceException, вызывается до того, как все блоки catch будут выполняться/разрешаться.
Ответы
Ответ 1
Событие TaskScheduler.UnobservedTaskException
должно дать вам то, что вы хотите, как вы сказали выше. Что заставляет вас думать, что он не уволен?
Исключения захватываются задачей, а затем повторно выбрасываются, но не сразу, в определенных ситуациях. Исключения из задач повторно выбрасываются несколькими способами (с верхней части головы, возможно, больше).
- При попытке доступа к результату (
Task.Result
)
- Вызов
Wait()
, Task.WaitOne()
, Task.WaitAll()
или другой связанный с ним метод Wait
для задачи.
- При попытке удалить задачу без явного поиска или обработки исключения
Если вы делаете что-либо из вышеперечисленного, исключение будет отвергнуто в любом потоке, на котором выполняется код, и событие не будет вызываться, так как вы будете наблюдать за исключением. Если у вас нет кода внутри try {} catch {}
, вы будете запускать AppDomain.CurrentDomain.UnhandledException
, который звучит так, как будто это может произойти.
В противном случае исключение будет повторно выбрано:
- Если вы ничего не делаете, чтобы задача все еще рассматривала исключение как ненаблюдаемое, и задача завершается. Это бросается в последнюю попытку, чтобы вы знали, что есть исключение, которого вы не видели.
Если это так, и поскольку финализатор не является детерминированным, ожидаете ли вы GC, чтобы эти задачи с ненаблюдаемыми исключениями помещались в очередь финализатора, а затем снова ожидали их завершения?
EDIT: Эта статья немного об этом говорит. И эта статья рассказывает о том, почему существует событие, которое может дать вам представление о том, как его можно использовать должным образом.
Ответ 2
Я использовал LimitedTaskScheduler из MSDN, чтобы поймать все исключения, включенные из других потоков, используя TPL:
public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
/// Whether the current thread is processing work items.
[ThreadStatic]
private static bool currentThreadIsProcessingItems;
/// The list of tasks to be executed.
private readonly LinkedList tasks = new LinkedList(); // protected by lock(tasks)
private readonly ILogger logger;
/// The maximum concurrency level allowed by this scheduler.
private readonly int maxDegreeOfParallelism;
/// Whether the scheduler is currently processing work items.
private int delegatesQueuedOrRunning; // protected by lock(tasks)
public LimitedConcurrencyLevelTaskScheduler(ILogger logger) : this(logger, Environment.ProcessorCount)
{
}
public LimitedConcurrencyLevelTaskScheduler(ILogger logger, int maxDegreeOfParallelism)
{
this.logger = logger;
if (maxDegreeOfParallelism Gets the maximum concurrency level supported by this scheduler.
public override sealed int MaximumConcurrencyLevel
{
get { return maxDegreeOfParallelism; }
}
/// Queues a task to the scheduler.
/// The task to be queued.
protected sealed override void QueueTask(Task task)
{
// Add the task to the list of tasks to be processed. If there aren't enough
// delegates currently queued or running to process tasks, schedule another.
lock (tasks)
{
tasks.AddLast(task);
if (delegatesQueuedOrRunning >= maxDegreeOfParallelism)
{
return;
}
++delegatesQueuedOrRunning;
NotifyThreadPoolOfPendingWork();
}
}
/// Attempts to execute the specified task on the current thread.
/// The task to be executed.
///
/// Whether the task could be executed on the current thread.
protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
// If this thread isn't already processing a task, we don't support inlining
if (!currentThreadIsProcessingItems)
{
return false;
}
// If the task was previously queued, remove it from the queue
if (taskWasPreviouslyQueued)
{
TryDequeue(task);
}
// Try to run the task.
return TryExecuteTask(task);
}
/// Attempts to remove a previously scheduled task from the scheduler.
/// The task to be removed.
/// Whether the task could be found and removed.
protected sealed override bool TryDequeue(Task task)
{
lock (tasks)
{
return tasks.Remove(task);
}
}
/// Gets an enumerable of the tasks currently scheduled on this scheduler.
/// An enumerable of the tasks currently scheduled.
protected sealed override IEnumerable GetScheduledTasks()
{
var lockTaken = false;
try
{
Monitor.TryEnter(tasks, ref lockTaken);
if (lockTaken)
{
return tasks.ToArray();
}
else
{
throw new NotSupportedException();
}
}
finally
{
if (lockTaken)
{
Monitor.Exit(tasks);
}
}
}
protected virtual void OnTaskFault(AggregateException exception)
{
logger.Error(exception);
}
///
/// Informs the ThreadPool that there work to be executed for this scheduler.
///
private void NotifyThreadPoolOfPendingWork()
{
ThreadPool.UnsafeQueueUserWorkItem(ExcuteTask, null);
}
private void ExcuteTask(object state)
{
// Note that the current thread is now processing work items.
// This is necessary to enable inlining of tasks into this thread.
currentThreadIsProcessingItems = true;
try
{
// Process all available items in the queue.
while (true)
{
Task item;
lock (tasks)
{
// When there are no more items to be processed,
// note that we're done processing, and get out.
if (tasks.Count == 0)
{
--delegatesQueuedOrRunning;
break;
}
// Get the next item from the queue
item = tasks.First.Value;
tasks.RemoveFirst();
}
// Execute the task we pulled out of the queue
TryExecuteTask(item);
if (!item.IsFaulted)
{
continue;
}
OnTaskFault(item.Exception);
}
}
finally
{
// We're done processing items on the current thread
currentThreadIsProcessingItems = false;
}
}
}
И чем "регистрация" TaskScheduler по умолчанию используется Reflection:
public static class TaskLogging
{
private const BindingFlags StaticBinding = BindingFlags.Static | BindingFlags.NonPublic;
public static void SetScheduler(TaskScheduler taskScheduler)
{
var field = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", StaticBinding);
field.SetValue(null, taskScheduler);
SetOnTaskFactory(new TaskFactory(taskScheduler));
}
private static void SetOnTaskFactory(TaskFactory taskFactory)
{
var field = typeof(Task).GetField("s_factory", StaticBinding);
field.SetValue(null, taskFactory);
}
}