简体   繁体   English

如何捕获/观察从Task抛出的未处理异常

[英]How to catch/observe an unhandled exception thrown from a Task

I'm trying to log / report all unhandled exceptions in my app (error reporting solution). 我正在尝试在我的应用程序中记录/报告所有未处理的异常(错误报告解决方案)。 I've come across a scenario that is always unhandled. 我遇到了一个总是未处理的情景。 I'm wondering how would I catch this error in an unhandled manner. 我想知道如何以未处理的方式捕获此错误。 Please note that I've done a ton of research this morning and tried a lot of things.. Yes, I've seen this , this and many more. 请注意,我今天早上做了很多研究并尝试了很多东西..是的,我已经看过这个这个以及更多。 I'm just looking for a generic solution to log unhandled exceptions. 我只是在寻找一种通用的解决方案来记录未处理的异常。

I have the following code inside of a console test apps main method: 我在控制台测试应用程序主要方法中有以下代码:

Task.Factory.StartNew(TryExecute);

or 要么

Task.Run((Action)TryExecute);

as well as the following method: 以及以下方法:

private static void TryExecute() {
   throw new Exception("I'm never caught");
}

I'm already tried wiring up to the following in my app, but they are never called. 我已经尝试在我的应用程序中连接到以下内容,但它们从未被调用过。

AppDomain.CurrentDomain.UnhandledException
TaskScheduler.UnobservedTaskException

In my Wpf app where I initially found this error I also wired up to these events but it was never called. 在我最初发现此错误的Wpf应用程序中,我也连接到这些事件,但它从未被调用过。

Dispatcher.UnhandledException
Application.Current.DispatcherUnhandledException
System.Windows.Forms.Application.ThreadException

The only handler that is called ever is: 唯一被称为的处理程序是:

AppDomain.CurrentDomain.FirstChanceException

but this is not a valid solution as I only want to report uncaught exceptions (not every exception as FirstChanceException is called before any catch blocks are ever executed / resolved. 但这不是一个有效的解决方案,因为我只想报告未捕获的异常(并非每个异常,因为在执行/解析任何catch块之前调用FirstChanceException。

The TaskScheduler.UnobservedTaskException event should give you what you want, as you stated above. 如上所述, TaskScheduler.UnobservedTaskException事件应该为您提供所需的内容。 What makes you think that it is not getting fired? 是什么让你觉得它没有被解雇?

Exceptions are caught by the task and then re-thrown, but not immediately , in specific situations. 在特定情况下,异常会被任务捕获,然后重新抛出, 但不会立即重新抛出。 Exceptions from tasks are re-thrown in several ways (off the top of my head, there are probably more). 任务的异常会以多种方式重新抛出(在我的脑海中,可能还有更多)。

  1. When you try and access the result ( Task.Result ) 当您尝试访问结果时( Task.Result
  2. Calling Wait() , Task.WaitOne() , Task.WaitAll() or another related Wait method on the task. 在任务上调用Wait()Task.WaitOne()Task.WaitAll()或其他相关的Wait方法。
  3. When you try to dispose the Task without explicitly looking at or handling the exception 当您尝试在不明确查看或处理异常的情况下处置Task

If you do any of the above, the exception will be rethrown on whatever thread that code is running on, and the event will not be called since you will be observing the exception. 如果您执行上述任何操作,则会在运行代码的任何线程上重新抛出异常, 并且由于您将观察异常, 因此不会调用该事件 If you don't have the code inside of a try {} catch {} , you will fire the AppDomain.CurrentDomain.UnhandledException , which sounds like what might be happening. 如果你没有try {} catch {}的代码,你将触发AppDomain.CurrentDomain.UnhandledException ,这听起来像是可能发生的事情。

The other way the exception is re-thrown would be: 重新抛出异常的另一种方式是:

  • When you do none of the above so that the task still views the exception as unobserved and the Task is getting finalized. 当您不执行上述任何操作时,任务仍然会将异常视为未观察到,并且任务正在完成。 It is thrown as a last ditch effort to let you know there was an exception that you didn't see. 它是最后的努力,让你知道有一个你没有看到的例外。

If this is the case and since the finalizer is non-deterministic, are you waiting for a GC to happen so that those tasks with unobserved exceptions are put in the finalizer queue, and then waiting again for them to be finalized? 如果是这种情况,并且由于终结器是非确定性的,您是否正在等待GC发生,以便将具有未观察到的异常的那些任务放入终结器队列中,然后再等待它们最终确定?

EDIT: This article talks a little bit about this. 编辑: 这篇文章谈到了这一点。 And this article talks about why the event exists, which might give you insight into how it can be used properly. 本文将讨论事件存在的原因,这可能会让您深入了解如何正确使用事件。

I used the LimitedTaskScheduler from MSDN to catch all exceptions, included from other threads using the TPL: 我使用MSDN中的LimitedTaskScheduler捕获所有异常,包括使用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's 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;
        }
    }
}

And than the "registration" of the TaskScheduler as the default using Reflection: 而不是使用Reflection将TaskScheduler的“注册”作为默认值:


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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM