Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 15 de febrero de 2022
.NET

Seguimos descubriendo novedades aparecidas con .NET 6, y ahora le toca el turno a la nueva clase PeriodicTimer, una fórmula para la ejecución de tareas periódicas en contextos asíncronos que evita el uso de los clásicos callbacks a los que nos tenía acostumbrados el framework.

Como recordaréis, .NET dispone de un buen número de formas de implementar temporizadores, o timers, para ejecutar tareas en intervalos periódicos. El más conocido probablemente sea el clásico System.Threading.Timer, en el que especificábamos el callback o método que debía ejecutarse en cada intervalo de tiempo mediante un delegado (en el siguiente ejemplo, mediante una lambda):

var timer = new System.Threading.Timer(o =>
{
    Console.WriteLine("Hey! " + DateTime.Now.ToLongTimeString());
}, null, 0, 1000);

Console.ReadKey();
Hey! 12:25:51
Hey! 12:25:52
Hey! 12:25:53
Hey! 12:25:54
Hey! 12:25:55
_

Pero también existía System.Timers.Timer, que nos permitía lograr algo parecido, aunque esta el callback lo implementábamos mediante una suscripción al evento Elapsed del objeto:

var timer = new System.Timers.Timer(1000);
timer.Elapsed += (sender, eventArgs) =>
{
    Console.WriteLine("Hey! " + DateTime.Now.ToLongTimeString());
};
timer.Start();
Console.ReadKey();

Existían algunas fórmulas más específicas para plataformas concretas, como las clases System.Windows.Forms.Timer, System.Web.UI.Timer u otras. Sin embargo, todas coincidían en varias cosas:

  • Utilizaban callbacks de alguna u otra forma, lo que implica un cierto riesgo de leaks de memoria y problemas con los tiempos de vida de objetos cuando la cosa se complica.
  • Los callbacks no permitían código asíncrono, lo que podía llevarnos a luchar contra los engorrosos escenarios de ejecución de código asíncrono en entornos síncronos (async-over-sync).
  • Podían darse casos de superposición u overlapping entre las distintas ejecuciones, cuando éstas tardaban en completarse más que el intervalo de definido en el timer.

La nueva clase PeriodicTimer elimina de un plumazo estos inconvenientes, al tratarse de una opción puramente asíncrona. A vista de pájaro, esta clase proporciona un temporizador cuyos ticks pueden ser esperados utilizando await, por lo que su implementación difiere bastante de las anteriores, al permitir un código más lineal, sin eventos, callbacks, ni nada parecido:

var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine("Hey! " + DateTime.Now.ToLongTimeString());
    // Obviamente, podemos incluir aquí código asíncrono
}

Hacer un await sobre el método WaitForNextTickAsync() provocará que la ejecución se detenga hasta el siguiente tick del temporizador, y retornará true mientras no haya sido cancelado, algo que podemos lograr de dos formas:

  • Bien liberando el objeto PeriodicTimer (implementa IDisposable).
  • O utilizando un token de cancelación.

Veamos el primer caso. El método Dispose() del objeto PeriodicTimer hace que la espera actual sea cancelada y que el método WaitForNextTickAsync() retorne el valor false. En el siguiente ejemplo, creamos un temporizador con intervalos de cinco segundos, pero la espera será cancelada a los siete segundos desde otro hilo liberando el temporizador, por lo que sólo dará tiempo a mostrar un mensaje:

var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
Task.Run(async () =>
{
    await Task.Delay(7000);
    timer.Dispose();
});

Console.WriteLine("Iniciando temporizador: " + DateTime.Now.ToLongTimeString());
while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine("Hey! " + DateTime.Now.ToLongTimeString());
}
Console.WriteLine("Temporizador finalizado: " + DateTime.Now.ToLongTimeString());

En tiempo de ejecución, podríamos ver lo siguiente:

Iniciando temporizador: 14:15:12
Hey! 14:15:17
Temporizador finalizado: 14:15:19
_

Como adelantábamos algo más arriba, otra posibilidad es utilizar tokens de cancelación. En el siguiente código vemos cómo reescribir el ejemplo anterior usando esta técnica:

using var ct = new CancellationTokenSource(7000);
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
Console.WriteLine("Iniciando temporizador: " + DateTime.Now.ToLongTimeString());
try
{
    while (await timer.WaitForNextTickAsync(ct.Token))
    {
        Console.WriteLine("Hey! " + DateTime.Now.ToLongTimeString());
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Temporizador finalizado: " + DateTime.Now.ToLongTimeString());
}

Fijaos que debemos envolver la llamada a WaitForNextTickAsync() en un bloque try-catch, porque si la cancelación se produce durante la espera, se lanza una excepción de tipo OperationCanceledException. En ejecución, veríamos algo bastante parecido a lo anterior:

Iniciando temporizador: 14:26:40
Hey! 14:26:45
Temporizador finalizado: 14:26:47
_

Por último, es importante añadir que, según la documentación oficial, PeriodicTimer sólo puede ser utilizado por un único consumidor al mismo tiempo, es decir, sólo puede existir una llamada en curso a WaitForNextTickAsync() en un momento dado.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!