Ответ 1
Здесь моя попытка:
public static class TaskExtensions {
/// <summary>
/// Issues the <paramref name="heartbeatAction"/> once every <paramref name="heartbeatInterval"/> while <paramref name="primaryTask"/> is running.
/// </summary>
public static async Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Action<CancellationToken> heartbeatAction, CancellationToken cancellationToken) {
if (cancellationToken.IsCancellationRequested) {
return;
}
var stopHeartbeatSource = new CancellationTokenSource();
cancellationToken.Register(stopHeartbeatSource.Cancel);
await Task.WhenAny(primaryTask, PerformHeartbeats(heartbeatInterval, heartbeatAction, stopHeartbeatSource.Token));
stopHeartbeatSource.Cancel();
}
private static async Task PerformHeartbeats(TimeSpan interval, Action<CancellationToken> heartbeatAction, CancellationToken cancellationToken) {
while (!cancellationToken.IsCancellationRequested) {
try {
await Task.Delay(interval, cancellationToken);
if (!cancellationToken.IsCancellationRequested) {
heartbeatAction(cancellationToken);
}
}
catch (TaskCanceledException tce) {
if (tce.CancellationToken == cancellationToken) {
// Totally expected
break;
}
throw;
}
}
}
}
или с небольшой настройкой вы можете даже сделать асинхронное сердцебиение так, как в:
/// <summary>
/// Awaits a fresh Task created by the <paramref name="heartbeatTaskFactory"/> once every <paramref name="heartbeatInterval"/> while <paramref name="primaryTask"/> is running.
/// </summary>
public static async Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Func<CancellationToken, Task> heartbeatTaskFactory, CancellationToken cancellationToken) {
if (cancellationToken.IsCancellationRequested) {
return;
}
var stopHeartbeatSource = new CancellationTokenSource();
cancellationToken.Register(stopHeartbeatSource.Cancel);
await Task.WhenAll(primaryTask, PerformHeartbeats(heartbeatInterval, heartbeatTaskFactory, stopHeartbeatSource.Token));
if (!stopHeartbeatSource.IsCancellationRequested) {
stopHeartbeatSource.Cancel();
}
}
public static Task WithHeartbeat(this Task primaryTask, TimeSpan heartbeatInterval, Func<CancellationToken, Task> heartbeatTaskFactory) {
return WithHeartbeat(primaryTask, heartbeatInterval, heartbeatTaskFactory, CancellationToken.None);
}
private static async Task PerformHeartbeats(TimeSpan interval, Func<CancellationToken, Task> heartbeatTaskFactory, CancellationToken cancellationToken) {
while (!cancellationToken.IsCancellationRequested) {
try {
await Task.Delay(interval, cancellationToken);
if (!cancellationToken.IsCancellationRequested) {
await heartbeatTaskFactory(cancellationToken);
}
}
catch (TaskCanceledException tce) {
if (tce.CancellationToken == cancellationToken) {
// Totally expected
break;
}
throw;
}
}
}
который позволит вам изменить пример кода на что-то вроде этого:
private static async Task PerformHeartbeat(CancellationToken cancellationToken) {
Console.WriteLine("Starting heartbeat {0}", ++_heartbeatCount);
await Task.Delay(1000, cancellationToken);
Console.WriteLine("Finishing heartbeat {0}", _heartbeatCount);
}
PerformHeartbeat можно заменить асинхронным вызовом, например RenewLockAsync, чтобы вам не пришлось тратить время потока с помощью блокирующего вызова например RenewLock, который потребует подход Action.
Я отвечаю на мой собственный вопрос в отношении SO-рекомендаций, но я также открыт для более элегантных подходов к этой проблеме.