Saltar al contenido principal

Zonas (Zones)

Gestiona tu código asíncrono: maneja errores no capturados, sobrescribe el comportamiento (como imprimir y programar tareas) y más.

Extensiones dinámicas asíncronas

#

Este artículo discute las API relacionadas con zonas en la biblioteca dart:async, enfocándose en las funciones de nivel superior runZoned() y runZonedGuarded() funciones. Revisa las técnicas cubiertas en Futures y manejo de errores antes de leer este artículo.

Las zonas hacen posibles las siguientes tareas:

  • Proteger tu aplicación para que no finalice debido a una excepción no capturada. Por ejemplo, un servidor HTTP simple podría usar el siguiente código asíncrono:

    dart
    runZonedGuarded(() {
      HttpServer.bind('0.0.0.0', port).then((server) {
        server.listen(staticFiles.serveRequest);
      });
    },
    (error, stackTrace) => print('Oh noes! $error $stackTrace'));
    

    Ejecutar el servidor HTTP en una zona permite que la aplicación continúe ejecutándose a pesar de los errores no capturados (pero no fatales) en el código asíncrono del servidor.

  • Asociar datos—conocidos como valores locales de la zonacon zonas individuales.

  • Sobrescribir un conjunto limitado de métodos, como print() y scheduleMicrotask(), en parte o en todo el código.

  • Realizar una operación cada vez que el código entra o sale de una zona. Tales operaciones podrían incluir iniciar o detener un temporizador, o guardar un rastreo de pila.

Es posible que te hayas encontrado con algo similar a las zonas en otros lenguajes. Los domains en Node.js fueron una inspiración para las zonas de Dart. El thread-local storage de Java también tiene algunas similitudes. Lo más parecido de todo es la adaptación a JavaScript de las zonas de Dart realizada por Brian Ford, zone.js, la cual describe en este video.

Conceptos básicos de las zonas

#

Una zona representa el alcance dinámico asíncrono de una llamada. Es la computación que se realiza como parte de una llamada y, transitivamente, los callbacks asíncronos que han sido registrados por ese código.

Por ejemplo, en el ejemplo del servidor HTTP, bind(), then() y el callback de then() se ejecutan todos en la misma zona—la zona que fue creada usando runZoned().

En el siguiente ejemplo, el código se ejecuta en 3 zonas diferentes: zona #1 (la zona raíz), zona #2 y zona #3.

import 'dart:async';

main() {
  foo();
  var future;
  runZoned(() {          // Starts a new child zone (zone #2).
    future = new Future(bar).then(baz);
  });
  future.then(qux);
}

foo() => ...foo-body...  // Executed twice (once each in two zones).
bar() => ...bar-body...
baz(x) => runZoned(() => foo()); // New child zone (zone #3).
qux(x) => ...qux-body...

La siguiente figura muestra el orden de ejecución del código, así como en qué zona se ejecuta el código.

ilustración de la ejecución del programa

Cada llamada a runZoned() crea una nueva zona y ejecuta el código en esa zona. Cuando ese código programa una tarea—como llamar a baz()—esa tarea se ejecuta en la zona donde fue programada. Por ejemplo, la llamada a qux() (última línea de main()) se ejecuta en zona #1 (la zona raíz) a pesar de que está asociada a un future que a su vez se ejecuta en zona #2.

Las zonas hijas no reemplazan completamente a su zona padre. En su lugar, las nuevas zonas se anidan dentro de la zona que las rodea. Por ejemplo, la zona #2 contiene a la zona #3, y la zona #1 (la zona raíz) contiene tanto a la zona #2 como a la zona #3.

Todo el código de Dart se ejecuta en la zona raíz. El código también podría ejecutarse en otras zonas hijas anidadas, pero como mínimo siempre se ejecuta en la zona raíz.

Manejo de errores no capturados

#

Las zonas pueden capturar y manejar errores no capturados.

Los errores no capturados a menudo ocurren debido a que el código usa throw para lanzar una excepción sin una sentencia catch que la acompañe para manejarla. Los errores no capturados también pueden surgir en funciones async cuando un Future se completa con un resultado de error, pero falta un await correspondiente para manejar el error.

Un error no capturado se reporta a la zona actual que falló en capturarlo. Por defecto, las zonas cerrarán el programa en respuesta a errores no capturados. Puedes instalar tu propio manejador de errores no capturados personalizado en una nueva zona para interceptar y manejar errores no capturados como prefieras.

Para introducir una nueva zona con un manejador de errores no capturados, usa el método runZonedGuarded. Su callback onError se convierte en el manejador de errores no capturados de la nueva zona. Este callback maneja cualquier error síncrono que la llamada lance.

dart
runZonedGuarded(() {
  Timer.run(() { throw 'Would normally kill the program'; });
}, (error, stackTrace) {
  print('Uncaught error: $error');
});

Other zone APIs that facilitate uncaught error handling include Zone.fork, Zone.runGuarded and ZoneSpecification.uncaughtErrorHandler.

El código anterior tiene un callback asíncrono (a través de Timer.run()) que lanza una excepción. Normalmente, esta excepción sería un error no manejado y llegaría al nivel superior (lo que, en el ejecutable independiente de Dart, terminaría el proceso en ejecución). Sin embargo, con el manejador de errores de zona, el error se pasa al manejador de errores y no detiene el programa.

Una diferencia notable entre try-catch y los manejadores de errores de zona es que las zonas continúan ejecutándose después de que ocurren errores no capturados. Si se programan otros callbacks asíncronos dentro de la zona, estos aún se ejecutan. Como consecuencia, un manejador de errores de zona podría ser invocado múltiples veces.

Cualquier zona con un manejador de errores no capturados se denomina una zona de error. Una zona de error podría manejar errores que se originen en una descendiente de esa zona. Una regla simple determina dónde se manejan los errores en una secuencia de transformaciones de future (usando then() o catchError()): Los errores en cadenas de Future nunca cruzan los límites de las zonas de error.

Si un error alcanza el límite de una zona de error, se trata como un error no manejado en ese punto.

Ejemplo: Los errores no pueden cruzar a zonas de error

#

En el siguiente ejemplo, el error lanzado por la primera línea no puede cruzar a una zona de error.

dart
var f = new Future.error(499);
f = f.whenComplete(() { print('Outside of zones'); });
runZoned(() {
  f = f.whenComplete(() { print('Inside non-error zone'); });
});
runZonedGuarded(() {
  f = f.whenComplete(() { print('Inside error zone (not called)'); });
}, (error) { print(error); });

Este es el resultado que ves si ejecutas el ejemplo:

Outside of zones
Inside non-error zone
Uncaught Error: 499
Unhandled exception:
499
...stack trace...

Si eliminas la llamada a runZoned() o a runZonedGuarded(), verás este resultado:

Outside of zones
Inside non-error zone
Inside error zone (not called)
Uncaught Error: 499
Unhandled exception:
499
...stack trace...

Nota cómo eliminar la zona o la zona de error hace que el error se propague más.

El rastreo de pila aparece porque el error ocurre fuera de una zona de error. Si agregas una zona de error alrededor de todo el fragmento de código, entonces puedes evitar el rastreo de pila.

Ejemplo: Los errores no pueden salir de las zonas de error

#

Como muestra el código anterior, los errores no pueden cruzar a zonas de error. De manera similar, los errores no pueden cruzar hacia afuera de las zonas de error. Considera este ejemplo:

dart
var completer = new Completer();
var future = completer.future.then((x) => x + 1);
var zoneFuture;
runZonedGuarded(() {
  zoneFuture = future.then((y) => throw 'Inside zone');
}, (error) { print('Caught: $error'); });

zoneFuture.catchError((e) { print('Never reached'); });
completer.complete(499);

Aunque la cadena de future termina en un catchError(), el error asíncrono no puede salir de la zona de error. El manejador de errores de zona que se encuentra en runZonedGuarded() maneja el error. Como resultado, zoneFuture nunca se completa — ni con un valor, ni con un error.

Usar zonas con streams

#

La regla para las zonas y los streams es más simple que para los futures:

Esta regla se deriva de la directriz de que los streams no deberían tener efectos secundarios hasta que sean escuchados. Una situación similar en el código síncrono es el comportamiento de los Iterables, los cuales no se evalúan hasta que solicitas valores.

Ejemplo: Usar un stream con runZonedGuarded()

#

El siguiente ejemplo configura un stream con un callback, y luego ejecuta ese stream en una nueva zona con runZonedGuarded():

dart
var stream = new File('stream.dart').openRead()
    .map((x) => throw 'Callback throws');

runZonedGuarded(() { stream.listen(print); },
         (e) { print('Caught error: $e'); });

El manejador de errores en runZonedGuarded() captura el error que lanza el callback. Aquí está la salida:

Caught error: Callback throws

Como muestra la salida, el callback está asociado con la zona de escucha, not con la zona donde se llama a map().

Almacenar valores locales de la zona

#

Si alguna vez quisiste usar una variable estática pero no pudiste porque múltiples computaciones que se ejecutan concurrentemente interferían entre sí, considera usar un valor local de la zona. Podrías agregar un valor local de la zona para ayudar con la depuración. Otro caso de uso es manejar una solicitud HTTP: podrías tener el ID de usuario y su token de autorización en valores locales de la zona.

Usa el argumento zoneValues en runZoned() para almacenar valores en la zona recién creada:

dart
runZoned(() {
  print(Zone.current[#key]);
}, zoneValues: { #key: 499 });

Para leer valores locales de la zona, usa el operador de índice de la zona y la clave del valor: [key]. Se puede usar cualquier objeto como clave, siempre que tenga implementaciones compatibles de operator == y hashCode. Típicamente, una clave es un símbolo literal: #identifier.

No puedes cambiar el objeto al que apunta una clave, pero puedes manipular el objeto. Por ejemplo, el siguiente código agrega un elemento a una lista local de la zona:

dart
runZoned(() {
  Zone.current[#key].add(499);
  print(Zone.current[#key]); // [499]
}, zoneValues: { #key: [] });

Una zona hereda los valores locales de la zona de su zona padre, por lo que agregar zonas anidadas no descarta accidentalmente los valores existentes. Sin embargo, las zonas anidadas pueden ocultar los valores del padre.

Ejemplo: Usar un valor local de la zona para logs de depuración

#

Supongamos que tienes dos archivos, foo.txt y bar.txt, y quieres imprimir todas sus líneas. El programa podría verse así:

dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

Future splitLinesStream(stream) {
  return stream
      .transform(ASCII.decoder)
      .transform(const LineSplitter())
      .toList();
}

Future splitLines(filename) {
  return splitLinesStream(new File(filename).openRead());
}
main() {
  Future.forEach(['foo.txt', 'bar.txt'],
                 (file) => splitLines(file)
                     .then((lines) { lines.forEach(print); }));
}

Este programa funciona, pero supongamos que ahora quieres saber de qué archivo proviene cada línea, y que no puedes simplemente agregar un argumento de nombre de archivo a splitLinesStream(). Con valores locales de la zona puedes agregar el nombre de archivo a la cadena devuelta (las nuevas líneas están resaltadas):

dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

Future splitLinesStream(stream) {
  return stream
      .transform(ASCII.decoder)
      .transform(const LineSplitter())
      .map((line) => '${Zone.current[#filename]}: $line')
      .toList();
}

Future splitLines(filename) {
  return runZoned(() {
    return splitLinesStream(new File(filename).openRead());
  }, zoneValues: { #filename: filename });
}

main() {
  Future.forEach(['foo.txt', 'bar.txt'],
                 (file) => splitLines(file)
                     .then((lines) { lines.forEach(print); }));
}

Nota que el nuevo código no modifica las firmas de las funciones ni pasa el nombre del archivo de splitLines() a splitLinesStream(). En su lugar, utiliza valores locales de la zona para implementar una característica similar a una variable estática que funciona en contextos asíncronos.

Sobrescribir funcionalidad

#

Usa el argumento zoneSpecification en runZoned() para sobrescribir la funcionalidad administrada por las zonas. El valor del argumento es un objeto ZoneSpecification con el cual puedes sobrescribir cualquiera de las siguientes funcionalidades:

  • Bifurcar zonas hijas
  • Registrar y ejecutar callbacks en la zona
  • Programar microtareas y temporizadores
  • Manejar errores asíncronos no capturados (runZonedGuarded() es un atajo para esto)
  • Imprimir

Ejemplo: Sobrescribir print

#

Como un ejemplo simple de cómo sobrescribir funcionalidad, aquí tienes una forma de silenciar todas las impresiones dentro de una zona:

dart
import 'dart:async';

main() {
  runZoned(() {
    print('Will be ignored');
  }, zoneSpecification: new ZoneSpecification(
    print: (self, parent, zone, message) {
      // Ignore message.
    }));
}

Dentro de la zona bifurcada, la función print() es sobrescrita por el interceptor de impresión especificado, el cual simplemente descarta el mensaje. Sobrescribir print es posible porque print() (como scheduleMicrotask() y los constructores de Timer) utiliza la zona actual (Zone.current) para realizar su trabajo.

Argumentos para interceptores y delegados

#

Como muestra el ejemplo de print, un interceptor agrega tres argumentos a los definidos en el método correspondiente de la clase Zone. Por ejemplo, el método print() de Zone tiene un argumento: print(String line). La versión del interceptor de print(), según lo define ZoneSpecification, tiene cuatro argumentos: print(Zone self, ZoneDelegate parent, Zone zone, String line).

Los tres argumentos del interceptor siempre aparecen en el mismo orden, antes de cualquier otro argumento.

self

La zona que está manejando el callback.

parent

Un ZoneDelegate que representa la zona padre. Úsalo para reenviar operaciones al padre.

zone

La zona donde se originó la operación. Algunas operaciones necesitan saber sobre qué zona se invocó la operación. Por ejemplo, zone.fork(specification) debe crear una nueva zona como hija de zone. Como otro ejemplo, incluso cuando delegas scheduleMicrotask() a otra zona, la zone original debe ser la que ejecute la microtask.

Cuando un interceptor delega un método al padre, la versión del método del padre (ZoneDelegate) tiene solo un argumento adicional: zone, la zona de donde se originó la llamada original. For example, la firma del método print() en un ZoneDelegate es print(Zone zone, String line).

Aquí hay un ejemplo de los argumentos para otro método interceptable, scheduleMicrotask():

| Dónde se define | Firma del método | | Zone | void scheduleMicrotask(void f()) | | ZoneSpecification  | void scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, void f()) | | ZoneDelegate | void scheduleMicrotask(Zone zone, void f()) |

Ejemplo: Delegar en la zona padre

#

Aquí hay un ejemplo que muestra cómo delegar en la zona padre:

dart
import 'dart:async';

main() {
  runZoned(() {
    var currentZone = Zone.current;
    scheduleMicrotask(() {
      print(identical(currentZone, Zone.current));  // prints true.
    });
  }, zoneSpecification: new ZoneSpecification(
    scheduleMicrotask: (self, parent, zone, task) {
      print('scheduleMicrotask has been called inside the zone');
      // The origin `zone` needs to be passed to the parent so that
      // the task can be executed in it.
      parent.scheduleMicrotask(zone, task);
    }));
}

Ejemplo: Ejecutar código al entrar y salir de una zona

#

Supongamos que quieres saber cuánto tiempo pasa ejecutándose algún código asíncrono. Puedes hacer esto colocando el código en una zona, iniciando un temporizador cada vez que se ingresa a la zona y deteniendo el temporizador cada vez que se sale de la zona.

Proporcionar parámetros run* a la ZoneSpecification te permite especificar el código que ejecuta la zona.

Los parámetros run*run, runUnary y runBinary—especifican el código a ejecutar cada vez que se le solicita a la zona que ejecute código. Estos parámetros funcionan para callbacks de cero argumentos, un argumento y dos argumentos, respectivamente. El parámetro run también funciona para el código síncrono inicial que se ejecuta justo después de llamar a runZoned().

Aquí hay un ejemplo de perfilado de código usando run*:

dart
final total = new Stopwatch();
final user = new Stopwatch();

final specification = new ZoneSpecification(
  run: (self, parent, zone, f) {
    user.start();
    try { return parent.run(zone, f); } finally { user.stop(); }
  },
  runUnary: (self, parent, zone, f, arg) {
    user.start();
    try { return parent.runUnary(zone, f, arg); } finally { user.stop(); }
  },
  runBinary: (self, parent, zone, f, arg1, arg2) {
    user.start();
    try {
      return parent.runBinary(zone, f, arg1, arg2);
    } finally {
      user.stop();
    }
  });

runZoned(() {
  total.start();
  // ... Code that runs synchronously...
  // ... Then code that runs asynchronously ...
    .then((...) {
      print(total.elapsedMilliseconds);
      print(user.elapsedMilliseconds);
    });
}, zoneSpecification: specification);

En este código, cada sobrescritura de run* simplemente inicia el temporizador de usuario, ejecutes la función especificada, y luego detiene el temporizador de usuario.

Ejemplo: Manejar callbacks

#

Proporciona parámetros register*Callback a la ZoneSpecification para envolver o cambiar el código del callback—el código que se ejecuta asíncronamente en la zona. Al igual que los parámetros run*, los parámetros register*Callback tienen tres formas: registerCallback (para callbacks sin argumentos), registerUnaryCallback (un argumento) y registerBinaryCallback (dos argumentos).

Aquí hay un ejemplo que hace que la zona guarde un rastreo de pila antes de que el código desaparezca en un contexto asíncrono.

dart
import 'dart:async';

get currentStackTrace {
  try {
    throw 0;
  } catch(_, st) {
    return st;
  }
}

var lastStackTrace = null;

bar() => throw "in bar";
foo() => new Future(bar);

main() {
  final specification = new ZoneSpecification(
    registerCallback: (self, parent, zone, f) {
      var stackTrace = currentStackTrace;
      return parent.registerCallback(zone, () {
        lastStackTrace = stackTrace;
        return f();
      });
    },
    registerUnaryCallback: (self, parent, zone, f) {
      var stackTrace = currentStackTrace;
      return parent.registerUnaryCallback(zone, (arg) {
        lastStackTrace = stackTrace;
        return f(arg);
      });
    },
    registerBinaryCallback: (self, parent, zone, f) {
      var stackTrace = currentStackTrace;
      return parent.registerBinaryCallback(zone, (arg1, arg2) {
        lastStackTrace = stackTrace;
        return f(arg1, arg2);
      });
    },
    handleUncaughtError: (self, parent, zone, error, stackTrace) {
      if (lastStackTrace != null) print("last stack: $lastStackTrace");
      return parent.handleUncaughtError(zone, error, stackTrace);
    });

  runZoned(() {
    foo();
  }, zoneSpecification: specification);
}

Adelante, ejecuta el ejemplo. Verás un rastreo de pila de "última pila" (lastStackTrace) que incluye a foo(), ya que foo() fue llamada síncronamente. El siguiente rastreo de pila (stackTrace) proviene del contexto asíncrono, que conoce bar() pero no foo().

Implementar callbacks asíncronos

#

Incluso si estás implementando una API asíncrona, es posible que no tengas que lidiar con zonas en absoluto. Por ejemplo, aunque podrías esperar que la biblioteca dart:io realice un seguimiento de las zonas actuales, en su lugar depende del manejo de zonas de las clases de dart:async como Future y Stream.

Si manejas las zonas explícitamente, entonces necesitas registrar todos los callbacks asíncronos y asegurarte de que cada callback se invoque en la zona donde se registró. Los métodos de ayuda bind*Callback de Zone facilitan esta tarea. Son atajos para register*Callback y run*, asegurando que cada callback esté registrado y se ejecute en esa Zone.

Si necesitas más control del que te proporciona bind*Callback, entonces necesitas usar register*Callback y run*. También es posible que desees usar los métodos run*Guarded de Zone, los cuales envuelven la llamada en un try-catch e invocan al uncaughtErrorHandler si ocurre un error.

Resumen

#

Las zonas son buenas para proteger tu código de excepciones no capturadas en el código asíncrono, pero pueden hacer mucho más. Puedes asociar datos con zonas y puedes sobrescribir la funcionalidad principal, como la impresión y la programación de tareas. Las zonas permiten una mejor depuración y proporcionan ganchos que puedes usar para funcionalidades como el perfilado.

Más recursos

#
Documentación de la API relacionada con zonas

Lee la documentación de runZoned(), runZonedGuarded(), Zone, ZoneDelegate y ZoneSpecification.

stack_trace

Con la clase Chain de la biblioteca stack_trace puedes obtener mejores rastreos de pila para el código ejecutado asíncronamente. Consulta el paquete stack_trace en el sitio pub.dev para obtener más información.

Más ejemplos

#

Aquí hay algunos ejemplos más complejos de uso de zonas.

El ejemplo de task_interceptor

La zona de juguete en task_interceptor.dart intercepta scheduleMicrotask, createTimer y createPeriodicTimer para simular el comportamiento de las primitivas de Dart sin ceder el paso al bucle de eventos.

El código fuente del paquete stack_trace

El paquete stack_trace utiliza zonas para formar cadenas de rastreos de pila para depurar código asíncrono. Las características de la zona utilizadas incluyen manejo de errores, valores locales de la zona y callbacks. Puedes encontrar el código fuente de stack_trace en el proyecto de GitHub de stack_trace.

El código fuente de dart:async

Estas dos bibliotecas del SDK implementan API que cuentan con callbacks asíncronos, y por lo tanto, lidian con zonas. Puedes explorar o descargar su código fuente bajo el directorio sdk/lib del proyecto de GitHub de Dart.

Agradecemos a Anders Johnsen y Lasse Reichstein Nielsen por sus revisiones de este artículo.

Parte del contenido de esta página podría estar desactualizado.