【发布时间】:2017-03-28 03:45:38
【问题描述】:
想要限制某些事件之间的间隔并在超出限制时采取措施并不罕见。例如,用于检测另一端是否处于活动状态的网络对等方之间的心跳消息。
在 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,如果有很多心跳包在小于阈值的时间内到达,那就是很多Timer对象在加载线程池。此外,每个Timer 的完成都会在线程池上运行代码,无论是否有任何继续链接到它。
你不能通过调用heartbeatLost.Dispose()来释放旧的Timer;这将给出一个例外
InvalidOperationException: 只有处于完成状态的任务才能被释放
可以创建CancellationTokenSource 并使用它来取消旧的延迟任务,但是当计时器本身具有可重新调度的特性时,创建更多对象来完成此任务似乎不是最佳选择。
集成计时器重新调度的最佳方法是什么,以便代码可以更像这样的结构?
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;
}
}
【问题讨论】:
-
如果在读取过程中心跳失败,读取是否也会失败?如果您确实需要手动心跳,这可能与您的阅读无关,例如在另一个可以向您的读取操作发送取消请求的健康监控循环中?
-
在第二次读取所需版本的代码时,“心跳”用于超时读取。不能为每次读取创建一个新的超时任务,或者直接在连接上设置超时吗?
-
您愿意将
Task heartbeatLost = idleTimeout.Task;移动到while (...)循环内吗?在循环之外,如果没有竞争条件,问题就很难解决。 -
@ScottChamberlain:是的,我假设延迟在触发后无法重置,但如果存在竞争条件,那么再次获取任务是完全可以的。
-
竞争条件是如果
heartbeatLost在WhenAny调用之后但在ResetDelay之前转换到Completed状态,那么你就搞砸了,因为你无法将任务移出 Completed一旦你进入它的状态。您必须为每个循环生成新的任务对象。
标签: c# timer async-await