with tpl programming parallel net library example completed async asp c# .net wpf exception-handling task-parallel-library

tpl - task is completed c#



Cómo capturar/observar una excepción no manejada lanzada desde una tarea (2)

Estoy intentando registrar / reportar todas las excepciones no manejadas en mi aplicación (solución de informe de errores). Me he encontrado con un escenario que siempre está sin manejar. Me pregunto cómo podría detectar este error de manera no controlada. Tenga en cuenta que he investigado un montón esta mañana y he probado muchas cosas ... Sí, he visto this , this y muchos más. Solo estoy buscando una solución genérica para registrar excepciones no manejadas.

Tengo el siguiente código dentro de un método principal de las aplicaciones de prueba de consola:

Task.Factory.StartNew(TryExecute);

o

Task.Run((Action)TryExecute);

así como el siguiente método:

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

Ya he intentado conectar lo siguiente en mi aplicación, pero nunca se llaman.

AppDomain.CurrentDomain.UnhandledException TaskScheduler.UnobservedTaskException

En mi aplicación Wpf, donde inicialmente encontré este error, también conecté estos eventos, pero nunca fue llamado.

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

El único manejador que se llama alguna vez es:

AppDomain.CurrentDomain.FirstChanceException

pero esta no es una solución válida, ya que solo deseo informar excepciones no detectadas (no todas las excepciones, ya que se llama a FirstChanceException antes de que se ejecute / resuelva cualquier bloque catch).


El evento TaskScheduler.UnobservedTaskException debería proporcionarle lo que desea, tal como se indicó anteriormente. ¿Qué te hace pensar que no está siendo despedido?

La tarea captura las excepciones y luego las vuelve a lanzar, pero no de forma inmediata , en situaciones específicas. Las excepciones de las tareas se vuelven a lanzar de varias maneras (por encima de mi cabeza, probablemente haya más).

  1. Cuando intentas y accedes al resultado ( Task.Result )
  2. Wait() llamada Wait() , Task.WaitOne() , Task.WaitAll() u otro método de Wait relacionado en la tarea.
  3. Cuando intenta deshacerse de la Tarea sin mirar explícitamente o manejar la excepción

Si realiza alguna de las acciones anteriores, la excepción volverá a aparecer en el hilo en el que se esté ejecutando el código y no se llamará al evento, ya que observará la excepción. Si no tiene el código dentro de un try {} catch {} , disparará la AppDomain.CurrentDomain.UnhandledException , que suena como lo que podría estar sucediendo.

La otra forma en que se relanza la excepción sería:

  • Cuando no realice ninguna de las acciones anteriores, la tarea seguirá viendo la excepción como inobservada y la tarea se finalizará. Se lanza como un último esfuerzo para informarle que hubo una excepción que no vio.

Si este es el caso y dado que el finalizador no es determinista, ¿está esperando que ocurra un GC para que esas tareas con excepciones no observadas se pongan en la cola del finalizador, y luego espere de nuevo para que se finalicen?

EDITAR: Este artículo habla un poco sobre esto. this habla sobre por qué existe el evento, lo que podría darle una idea de cómo puede usarse correctamente.


Utilicé el LimitedTaskScheduler de MSDN para detectar todas las excepciones, incluidas las de otros subprocesos que usan el 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; } } }

Y que el "registro" de TaskScheduler como predeterminado utilizando 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); } }