Как поймать/наблюдать необработанное исключение, брошенное из Задачи

Я пытаюсь регистрировать/сообщать обо всех необработанных исключениях в моем приложении (решение для отчетов об ошибках). Я столкнулся с сценарием, который всегда необработан. Мне интересно, как я поймаю эту ошибку необработанным образом. Обратите внимание, что сегодня утром я провел много исследований и много чего пытался. Да, я видел этот, и многое другое. Я просто ищу универсальное решение для регистрации необработанных исключений.

У меня есть следующий код внутри основного метода консольных тестовых приложений:

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