Control de excepciones
Para ver el artículo en inglés, active la casilla Inglés. También puede ver el texto en inglés en una ventana emergente si pasa el puntero del mouse por el texto.
Traducción
Inglés

Control de excepciones (Task Parallel Library)

.NET Framework (current version)
 

Las excepciones no controladas que se inician mediante código de usuario que se ejecuta dentro de una tarea se propagan de vuelta al subproceso que hace la llamada, excepto en determinados escenarios que se describen posteriormente en este tema. Las excepciones se propagan cuando se usa uno de los métodos estáticos o de instancia Task.Wait o Task<TResult>.Wait, los cuales se pueden controlar si se incluye la llamada en una instrucción try/catch. Si una tarea es la tarea primaria de tareas secundarias asociadas o si se esperan varias tareas, pueden producirse varias excepciones.

Para propagar todas las excepciones de nuevo al subproceso que realiza la llamada, la infraestructura de la tarea las encapsula en una instancia de AggregateException. La propiedad SIDHistory hace esto posible. También puede controlar las excepciones originales mediante el método AggregateException.Handle.

Incluso aunque solo se produzca una excepción, se encapsulará en una excepción AggregateException, como se muestra en el ejemplo siguiente.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var task1 = Task.Run( () => { throw new CustomException("This exception is expected!"); } );

      try
      {
          task1.Wait();
      }
      catch (AggregateException ae)
      {
          foreach (var e in ae.InnerExceptions) {
              // Handle the custom exception.
              if (e is CustomException) {
                  Console.WriteLine(e.Message);
              }
              // Rethrow any other exception.
              else {
                  throw;
              }
          }
      }
   }
}

public class CustomException : Exception
{
   public CustomException(String message) : base(message)
   {}
}
// The example displays the following output:
//        This exception is expected!

Para evitar una excepción no controlada, basta con detectar el objeto AggregateException y omitir las excepciones internas. Sin embargo, esta operación no resulta recomendable porque es igual que detectar el tipo Exception base en escenarios no paralelos. Si desea detectar una excepción sin realizar acciones concretas que la resuelvan, puede dejar al programa en un estado indeterminado.

Si no desea llamar a los métodos Task.Wait ni Task<TResult>.Wait para esperar a la finalización de una tarea, también puede recuperar la excepción AggregateException de propiedad Exception de la tarea, como se muestra en el ejemplo siguiente. Para más información, consulte la sección Observar excepciones mediante la propiedad Task.Exception de este tema.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var task1 = Task.Run( () => { throw new CustomException("This exception is expected!"); } );

      while(! task1.IsCompleted) {}

      if (task1.Status == TaskStatus.Faulted) {
          foreach (var e in task1.Exception.InnerExceptions) {
              // Handle the custom exception.
              if (e is CustomException) {
                  Console.WriteLine(e.Message);
              }
              // Rethrow any other exception.
              else {
                  throw e;
              }
          }
      }
   }
}

public class CustomException : Exception
{
   public CustomException(String message) : base(message)
   {}
}
// The example displays the following output:
//        This exception is expected!

Si no espera a una tarea que propague la excepción ni accede a su propiedad Exception, la excepción se escalará conforme a la directiva de excepciones de .NET cuando la tarea se recopile como elemento no utilizado.

Cuando las excepciones pueden propagarse de vuelta al subproceso de unión, es posible que una tarea continúe procesando algunos elementos después de que se haya producido la excepción.

System_CAPS_noteNota

Cuando está habilitada la opción "Solo mi código", en algunos casos, Visual Studio se interrumpe en la línea que produce la excepción y muestra el mensaje de error "Excepción no controlada por el código de usuario". Este error es benigno. Puede presionar F5 para continuar y ver el comportamiento de control de excepciones que se muestra en estos ejemplos. Para evitar que Visual Studio se interrumpa con el primer error, desactive la casilla Habilitar Solo mi código bajo Herramientas, Opciones, Depuración, General.

Si una tarea tiene una tarea secundaria adjunta que inicia una excepción, esa excepción se encapsula en un objeto AggregateException antes de que se propague a la tarea primaria, que encapsula esa excepción en su propio objeto AggregateException antes de propagarla de nuevo al subproceso que realiza la llamada. En casos como este, la propiedad InnerExceptions de la excepción AggregateException que se detecta en los métodos Task.Wait, Task<TResult>.Wait, WaitAny o WaitAll contiene una o varias instancias de AggregateException, pero no las excepciones originales que produjeron el error. Para evitar tener que iterar sobre excepciones AggregateException, puede usar el método Flatten para quitar todas las excepciones AggregateException anidadas, de forma que la propiedad AggregateException.InnerExceptions contenga las excepciones originales. En el ejemplo siguiente, las instancias anidadas de AggregateException se reducen y se controlan en un solo bucle.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var task1 = Task.Factory.StartNew(() => {
                     var child1 = Task.Factory.StartNew(() => {
                        var child2 = Task.Factory.StartNew(() => {
                            // This exception is nested inside three AggregateExceptions.
                            throw new CustomException("Attached child2 faulted.");
                        }, TaskCreationOptions.AttachedToParent);

                        // This exception is nested inside two AggregateExceptions.
                        throw new CustomException("Attached child1 faulted.");
                     }, TaskCreationOptions.AttachedToParent);
      });

      try {
         task1.Wait();
      }
      catch (AggregateException ae) {
         foreach (var e in ae.Flatten().InnerExceptions) {
            if (e is CustomException) {
               Console.WriteLine(e.Message);
            }
            else {
               throw;
            }
         }
      }
   }
}

public class CustomException : Exception
{
   public CustomException(String message) : base(message)
   {}
}
// The example displays the following output:
//    Attached child1 faulted.
//    Attached child2 faulted.

También puede utilizar el método AggregateException.Flatten para volver a generar las excepciones internas de varias instancias de AggregateException iniciadas por varias tareas en una sola instancia de AggregateException, como se muestra en el ejemplo siguiente.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
public class Example
{
   public static void Main()
   {
       try {
          ExecuteTasks();
       }
       catch (AggregateException ae) {
          foreach (var e in ae.InnerExceptions)
             Console.WriteLine("{0}:\n   {1}", e.GetType().Name, e.Message);

       }
   }

   static void ExecuteTasks()
   {
        // Assume this is a user-entered String.
        String path = @"C:\";
        List<Task> tasks = new List<Task>();

        tasks.Add(Task.Run(() => {
                             // This should throw an UnauthorizedAccessEXception.
                              return Directory.GetFiles(path, "*.txt",
                                                        SearchOption.AllDirectories);
                           } ));

        tasks.Add(Task.Run(() => {
                              if (path == @"C:\")
                                 throw new ArgumentException("The system root is not a valid path.");
                              return new String[] { ".txt", ".dll", ".exe", ".bin", ".dat" };
                           } ));

        tasks.Add(Task.Run( () => {
                               throw new NotImplementedException("This operation has not been implemented.");
                           } ));

        try {
            Task.WaitAll(tasks.ToArray());
        }
        catch (AggregateException ae) {
            throw ae.Flatten();
        }
    }
}
// The example displays the following output:
//       UnauthorizedAccessException:
//          Access to the path 'C:\Documents and Settings' is denied.
//       ArgumentException:
//          The system root is not a valid path.
//       NotImplementedException:
//          This operation has not been implemented.

De forma predeterminada, las tareas secundarias están desasociadas cuando se crean. Las excepciones producidas por tareas desasociadas deben controlarse o reiniciarse en la tarea primaria inmediata; no se propagan de nuevo al subproceso que realiza la llamada del mismo modo que las tareas secundarias asociadas. La tarea primaria superior puede reiniciar manualmente una excepción de una tarea secundaria desasociada para que se encapsule en un objeto AggregateException y propagarla de vuelta al subproceso de unión.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var task1 = Task.Run(() => {
                       var nested1 = Task.Run(() => {
                                          throw new CustomException("Detached child task faulted.");
                                     });

          // Here the exception will be escalated back to the calling thread.
          // We could use try/catch here to prevent that.
          nested1.Wait();
      });

      try {
         task1.Wait();
      }
      catch (AggregateException ae) {
         foreach (var e in ae.Flatten().InnerExceptions) {
            if (e is CustomException) {
               Console.WriteLine(e.Message);
            }
         }
      }
   }
}

public class CustomException : Exception
{
   public CustomException(String message) : base(message)
   {}
}
// The example displays the following output:
//    Detached child task faulted.

Aunque se use una tarea de continuación para observar una excepción en una tarea secundaria, la tarea primaria debe seguir observando la excepción.

Cuando el código de usuario de una tarea responde a una solicitud de cancelación, el procedimiento correcto es producir una excepción OperationCanceledException que se pasa en el token de cancelación con el que se comunicó la solicitud. Antes de intentar propagar la excepción, la instancia de la tarea compara el token de la excepción con el que recibió durante su creación. Si son iguales, la tarea propaga una excepción TaskCanceledException encapsulada en un elemento AggregateException y puede verse cuando se examinan las excepciones internas. Sin embargo, si el subproceso que hace la llamada no está esperando la tarea, no se propagará esa excepción concreta. Para obtener más información, consulta Task Cancellation.

var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

var task1 = Task.Factory.StartNew(() =>
{
    CancellationToken ct = token;
    while (someCondition)
    {
        // Do some work...
        Thread.SpinWait(50000);
        ct.ThrowIfCancellationRequested();
    }
},
token);

// No waiting required.
tokenSource.Dispose();

El método AggregateException.Handle puede usarse para filtrar excepciones que pueden tratarse como "controladas" sin necesidad de usar ninguna otra lógica. En el delegado de usuario que se facilita al método AggregateException.Handle(Func<Exception, Boolean>), se puede examinar el tipo de excepción, su propiedad Message o cualquier otra información sobre ella que permita determinar si no supone un riesgo. Las excepciones en las que el delegado devuelve false se reinician inmediatamente en una nueva instancia de AggregateException después de que el método AggregateException.Handle devuelva un valor.

El ejemplo siguiente es funcionalmente equivalente al primer ejemplo de este tema, que examina cada excepción de la colección AggregateException.InnerExceptions. En su lugar, este controlador de excepciones llama al objeto de método AggregateException.Handle por cada excepción y solo vuelve a generar las excepciones que no son instancias de CustomException.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var task1 = Task.Run( () => { throw new CustomException("This exception is expected!"); } );

      try {
          task1.Wait();
      }
      catch (AggregateException ae)
      {
         // Call the Handle method to handle the custom exception,
         // otherwise rethrow the exception.
         ae.Handle(ex => { if (ex is CustomException)
                             Console.WriteLine(ex.Message);
                          return ex is CustomException;
                        });
      }
   }
}

public class CustomException : Exception
{
   public CustomException(String message) : base(message)
   {}
}
// The example displays the following output:
//        This exception is expected!

A continuación se muestra un ejemplo más completo que usa el método AggregateException.Handle para ofrecer un control especial para una excepción UnauthorizedAccessException al enumerar los archivos.

using System;
using System.IO;
using System.Threading.Tasks;

public class Example
{
    public static void Main()
    {
        // This should throw an UnauthorizedAccessException.
       try {
           var files = GetAllFiles(@"C:\");
           if (files != null)
              foreach (var file in files)
                 Console.WriteLine(file);
        }
        catch (AggregateException ae) {
           foreach (var ex in ae.InnerExceptions)
               Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);
        }
        Console.WriteLine();

        // This should throw an ArgumentException.
        try {
           foreach (var s in GetAllFiles(""))
              Console.WriteLine(s);
        }
        catch (AggregateException ae) {
           foreach (var ex in ae.InnerExceptions)
               Console.WriteLine("{0}: {1}", ex.GetType().Name, ex.Message);
        }
    }

    static string[] GetAllFiles(string path)
    {
       var task1 = Task.Run( () => Directory.GetFiles(path, "*.txt",
                                                      SearchOption.AllDirectories));

       try {
          return task1.Result;
       }
       catch (AggregateException ae) {
          ae.Handle( x => { // Handle an UnauthorizedAccessException
                            if (x is UnauthorizedAccessException) {
                                Console.WriteLine("You do not have permission to access all folders in this path.");
                                Console.WriteLine("See your network administrator or try another path.");
                            }
                            return x is UnauthorizedAccessException;
                          });
          return Array.Empty<String>();
       }
   }
}
// The example displays the following output:
//       You do not have permission to access all folders in this path.
//       See your network administrator or try another path.
//
//       ArgumentException: The path is not of a legal form.

Si una tarea se completa con el estado TaskStatus.Faulted, se puede examinar su propiedad Exception para detectar qué excepción concreta produjo el error. Un mecanismo adecuado para observar la propiedad Exception es usar una continuación que se ejecute solo si se produce un error en la tarea anterior, tal y como se muestra en el siguiente ejemplo.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var task1 = Task.Run(() =>
                           { throw new CustomException("task1 faulted.");
      }).ContinueWith( t => { Console.WriteLine("{0}: {1}",
                                                t.Exception.InnerException.GetType().Name,
                                                t.Exception.InnerException.Message);
                            }, TaskContinuationOptions.OnlyOnFaulted);
      Thread.Sleep(500);
   }
}

public class CustomException : Exception
{
   public CustomException(String message) : base(message)
   {}
}
// The example displays output like the following:
//        CustomException: task1 faulted.

En una aplicación real, el delegado de continuación podría registrar información detallada sobre la excepción y posiblemente generar nuevas tareas para recuperarse de la excepción.

En algunos escenarios (por ejemplo, cuando se hospedan complementos que no son de confianza), es posible que se produzcan numerosas excepciones benignas y que resulte demasiado difícil observarlas todas manualmente. En estos casos, se puede proceder a controlar el evento TaskScheduler.UnobservedTaskException. La propiedad SIDHistory hace esto posible.

Mostrar:
© 2016 Microsoft