c# timer async-await

c# - Detección de inactividad basada en tareas



timer async-await (1)

Me parece muy sencillo: el nombre de tu clase hipotética te lleva la mayor parte del camino hasta allí. Todo lo que necesitas es TaskCompletionSource y un temporizador único que sigues restableciendo.

public class TaskDelayedCompletionSource { private TaskCompletionSource<bool> _completionSource; private readonly System.Threading.Timer _timer; private readonly object _lockObject = new object(); public TaskDelayedCompletionSource(int interval) { _completionSource = CreateCompletionSource(); _timer = new Timer(OnTimerCallback); _timer.Change(interval, Timeout.Infinite); } private static TaskCompletionSource<bool> CreateCompletionSource() { return new TaskCompletionSource<bool>(TaskCreationOptions.DenyChildAttach | TaskCreationOptions.RunContinuationsAsynchronously | TaskCreationOptions.HideScheduler); } private void OnTimerCallback(object state) { //Cache a copy of the completion source before we entier the lock, so we don''t complete the wrong source if ResetDelay is in the middle of being called. var completionSource = _completionSource; lock (_lockObject) { completionSource.TrySetResult(true); } } public void ResetDelay(int interval) { lock (_lockObject) { var oldSource = _completionSource; _timer.Change(interval, Timeout.Infinite); _completionSource = CreateCompletionSource(); oldSource.TrySetCanceled(); } } public Task Task => _completionSource.Task; }

Esto solo creará un solo temporizador y lo actualizará, la tarea se completará cuando el temporizador se dispare.

Tendrá que cambiar un poco su código, porque se crea un nuevo TaskCompletionSource cada vez que actualiza la hora de finalización que necesita para poner la Task heartbeatLost = idleTimeout.Task; llamar dentro del ciclo while.

var client = new TcpClient { ... }; await client.ConnectAsync(...); var idleTimeout = new TaskDelayedCompletionSource(HEARTBEAT_LOST_THRESHOLD); while (...) { Task heartbeatLost = idleTimeout.Task; Task<int> readTask = client.ReadAsync(buffer, 0, buffer.Length); Task first = await Task.WhenAny(heartbeatLost, readTask); if (first == readTask) { if (ProcessData(buffer, 0, readTask.Result).HeartbeatFound) { idleTimeout.ResetDelay(HEARTBEAT_LOST_THRESHOLD); } } else if (first == heartbeatLost) { TellUserPeerIsDown(); } }

EDITAR: Si fuiste asesorado sobre la creación del objeto de las fuentes de finalización (por ejemplo, estás programando en un Motor de juego donde la recopilación de GC es una gran conserjería), puedes agregar lógica extra a OnTimerCallback y ResetDelay para reutilizar la fuente de finalización si la llamada aún no ha sucedido y seguro que no está dentro de un Retardo de reinicio.

Es probable que deba cambiar de usar un lock a un SemaphoreSlim y cambiar la devolución de llamada a

private void OnTimerCallback(object state) { if(_semaphore.Wait(0)) { _completionSource.TrySetResult(true); } }

Puedo actualizar esta respuesta más tarde para incluir lo que OnTimerCallback también tendría, pero no tengo tiempo en este momento.

No es inusual querer un límite en el intervalo entre ciertos eventos, y tomar medidas si se excede el límite. Por ejemplo, un mensaje de latido entre pares de la red para detectar que el otro extremo está activo.

En el estilo C # async / await, es posible implementar eso reemplazando la tarea de tiempo de espera cada vez que llega el latido del corazón:

var client = new TcpClient { ... }; await client.ConnectAsync(...); Task heartbeatLost = new Task.Delay(HEARTBEAT_LOST_THRESHOLD); while (...) { Task<int> readTask = client.ReadAsync(buffer, 0, buffer.Length); Task first = await Task.WhenAny(heartbeatLost, readTask); if (first == readTask) { if (ProcessData(buffer, 0, readTask.Result).HeartbeatFound) { heartbeatLost = new Task.Delay(HEARTBEAT_LOST_THRESHOLD); } } else if (first == heartbeatLost) { TellUserPeerIsDown(); break; } }

Esto es conveniente, pero cada instancia de la Task retardo posee un Timer , y si llegan muchos paquetes de latidos en menos tiempo que el umbral, son muchos los objetos de Timer que cargan el subproceso de subprocesos. Además, la finalización de cada Timer ejecutará el código en el grupo de subprocesos, independientemente de si hay alguna continuación vinculada a él o no.

No puede liberar el Timer llamando a heartbeatLost.Dispose() ; eso dará una excepción

InvalidOperationException : una tarea solo se puede eliminar si está en estado de finalización

Uno podría crear un CancellationTokenSource y usarlo para cancelar la vieja tarea de demora, pero parece que no es óptimo crear aún más objetos para lograr esto, cuando los temporizadores tienen la característica de ser reprogramables.

¿Cuál es la mejor manera de integrar la reprogramación del temporizador para que el código se pueda estructurar más como este?

var client = new TcpClient { ... }; await client.ConnectAsync(...); var idleTimeout = new TaskDelayedCompletionSource(HEARTBEAT_LOST_THRESHOLD); Task heartbeatLost = idleTimeout.Task; while (...) { Task<int> readTask = client.ReadAsync(buffer, 0, buffer.Length); Task first = await Task.WhenAny(heartbeatLost, readTask); if (first == readTask) { if (ProcessData(buffer, 0, readTask.Result).HeartbeatFound) { idleTimeout.ResetDelay(HEARTBEAT_LOST_THRESHOLD); } } else if (first == heartbeatLost) { TellUserPeerIsDown(); break; } }