基于任务的空闲检测

时间:2017-03-27 16:54:52

标签: c# timer async-await

希望限制某些事件之间的间隔并且如果超出限制则采取措施并不罕见。例如,网络对等体之间的心跳消息,用于检测另一端是否存活。

在C#async / await样式中,可以通过每次心跳到达时替换超时任务来实现:

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;
    }
}

这很方便,但延迟Task的每个实例都拥有一个Timer,如果许多心跳包在比阈值更短的时间内到达,那么很多{{1}加载线程池的对象。此外,每个Timer的完成都将在线程池上运行代码,无论是否有任何延续仍然与它相关联。

您无法通过拨打Timer释放旧Timer;那将给出例外

  

heartbeatLost.Dispose():任务只有在处于完成状态时才会被处置

可以创建一个InvalidOperationException并使用它来取消旧的延迟任务,但是当定时器本身具有可重新调度的功能时,创建更多对象来完成此任务似乎不是最理想的。

什么是集成计时器重新安排的最佳方式,以便代码可以更像这样构建?

CancellationTokenSource

1 个答案:

答案 0 :(得分:4)

对我来说似乎很简单,你的假设课程的名字可以让你大部分时间到达那里。您只需要一个TaskCompletionSource和一个可以重置的计时器。

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;
}

这只会创建一个计时器并对其进行更新,当计时器触发时,任务就会完成。

您需要稍微更改一下代码,因为每次更新需要将Task heartbeatLost = idleTimeout.Task;调用放在while循环中的结束时间时,都会创建一个新的TaskCompletionSource。

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();
    }
}

编辑:如果您对完成源的对象创建有所了解(例如,您在游戏引擎中进行编程,其中GC集合是一个很大的概念),您可以添加额外的逻辑如果呼叫尚未发生,则OnTimerCallbackResetDelay重新使用完成源,并且您确定不在重置延迟范围内。

您可能需要从使用lock切换到SemaphoreSlim并将回调更改为

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

我稍后可能会更新此答案,以包含OnTimerCallback也会包含的内容,但我现在没有时间。

相关问题