Ejecute el método asíncrono regularmente con el intervalo especificado
Necesito publicar algunos datos en el servicio desde la aplicación web C#. Los datos en sí se recopilan cuando el usuario utiliza la aplicación (una especie de estadísticas de uso). No quiero enviar datos al servicio durante la solicitud de cada usuario, prefiero recopilar los datos en la aplicación y luego enviar todos los datos en una sola solicitud en un hilo separado, que no atiende las solicitudes de los usuarios (quiero decir el usuario no tiene que esperar a que el servicio procese la solicitud). Para ello necesito una especie de setInterval
análogo de JS: el lanzamiento de la función cada X segundos para enviar todos los datos recopilados al servicio.
Descubrí que Timer
la clase proporciona algo similar ( Elapsed
evento). Sin embargo, esto permite ejecutar el método sólo una vez, pero eso no es un gran problema. La principal dificultad es que requiere la firma.
void MethodName(object e, ElapsedEventArgs args)
Si bien me gustaría iniciar el método asíncrono, éste llamará al servicio web (los parámetros de entrada no son importantes):
async Task MethodName(object e, ElapsedEventArgs args)
¿Alguien podría aconsejar cómo resolver la tarea descrita? Se agradece cualquier consejo.
El async
equivalente es un while
bucle con Task.Delay
(que internamente utiliza a System.Threading.Timer
):
public async Task PeriodicFooAsync(TimeSpan interval, CancellationToken cancellationToken)
{
while (true)
{
await FooAsync();
await Task.Delay(interval, cancellationToken)
}
}
Es importante pasar a CancellationToken
para que pueda detener esa operación cuando lo desee (por ejemplo, cuando cierre su aplicación).
Ahora bien, si bien esto es relevante para .Net en general, en ASP.Net es peligroso realizar cualquier tipo de incendio y olvidarse. Hay varias soluciones para esto (como HangFire), algunas están documentadas en Fire and Forget on ASP.NET de Stephen Cleary, otras en Cómo ejecutar tareas en segundo plano en ASP.NET de Scott Hanselman.
A continuación se muestra un método que invoca un método asincrónico de forma periódica:
public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
CancellationToken cancellationToken = default)
{
while (true)
{
Task delayTask = Task.Delay(interval, cancellationToken);
await action();
await delayTask;
}
}
El suministrado action
se invoca cada vez interval
y luego Task
se espera el creado. La duración de la espera no afecta al intervalo, a menos que sea mayor. En tal caso prevalecerá el principio de no superposición de ejecución, por lo que el plazo se ampliará hasta igualar la duración de la espera.
En caso de excepción, la PeriodicAsync
tarea se completará con error, por lo que si desea que sea resistente a errores, debe incluir un manejo riguroso de errores dentro del archivo action
.
Ejemplo de uso:
Task statisticsUploader = PeriodicAsync(async () =>
{
try
{
await UploadStatisticsAsync();
}
catch (Exception ex)
{
// Log the exception
}
}, TimeSpan.FromMinutes(5));
Actualización de .NET 6: ahora es posible implementar una funcionalidad casi idéntica sin incurrir en el costo de una Task.Delay
asignación en cada bucle, utilizando la nueva PeriodicTimer
clase:
public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
CancellationToken cancellationToken = default)
{
using PeriodicTimer timer = new(interval);
while (true)
{
await action();
await timer.WaitForNextTickAsync(cancellationToken);
}
}
El WaitForNextTickAsync
método devuelve a ValueTask<bool>
, que es lo que hace que esta implementación sea más eficiente. Sin embargo, la diferencia en eficiencia es bastante minúscula. Para una acción periódica que se ejecuta cada 5 minutos, asignar algunos objetos livianos en cada iteración debería tener un impacto prácticamente nulo.
El comportamiento de la PeriodicTimer
implementación basada en -no es idéntico al de la Task.Delay
implementación basada en -. En caso de que la duración de una acción sea mayor que interval
, ambas implementaciones invocarán la siguiente acción inmediatamente después de completar la acción anterior, pero el programador de la PeriodicTimer
implementación basada en -no avanzará como Task.Delay
lo hace la implementación basada en -. Consulte el diagrama de mármol a continuación para ver una demostración visual de la diferencia:
Clock X---------X---------X---------X---------X---------X---------X--
Task.Delay: +-----| +---| +------------|+---| +------| +--|
PeriodicTimer: +-----| +---| +------------|+---| +------| +--| +--
La programación de la Task.Delay
implementación basada en - se adelantó permanentemente, porque la tercera invocación de la action
duró más que la interval
.
Como puede ver, PeriodicTimer
no garantiza un intervalo mínimo entre invocaciones del archivo action
.
Las PeriodicAsync
implementaciones anteriores se han mantenido simples intencionalmente con fines educativos. Cosas como la validación de argumentos y ConfigureAwait
se han omitido.