Last active
October 13, 2023 11:29
-
-
Save in-async/b5443d49c4563c52919d027bdf8b20f3 to your computer and use it in GitHub Desktop.
定期的に自動更新される値を表すクラス。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// 定期的に自動更新される値を表すクラス。 | |
/// </summary> | |
public sealed class Later<T> : IDisposable { | |
private readonly CancellationTokenSource _disposingCts = new(); | |
private readonly CancellationTokenSource _stoppingCts; | |
private Task<T> _valueTask; | |
public Later(Func<CancellationToken, Task<T>> factory, TimeSpan period, CancellationToken stoppingToken = default) | |
: this(factory, period, LaterErrorHandling.Capture, stoppingToken) { | |
} | |
/// <param name="factory">値を生成するデリゲート。</param> | |
/// <param name="period"><paramref name="factory"/> の最小呼び出し間隔。</param> | |
/// <param name="errorHandling"><paramref name="factory"/> がエラーを投げた時の動作。</param> | |
/// <param name="stoppingToken">値の自動更新を停止する為のトークン。</param> | |
/// <remarks> | |
/// <paramref name="stoppingToken"/> や <see cref="Stop"/> は値の自動更新を停止しますが、 <paramref name="factory"/> はキャンセルしません。 | |
/// <paramref name="factory"/> は <see cref="Dispose()"/> やファイナライズされた時のみキャンセルされます。 | |
/// </remarks> | |
public Later(Func<CancellationToken, Task<T>> factory, TimeSpan period, LaterErrorHandling errorHandling, CancellationToken stoppingToken = default) { | |
if (factory is null) { throw new ArgumentNullException(nameof(factory)); } | |
TaskCompletionSource<T> firstTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); | |
_valueTask = firstTcs.Task; | |
CancellationToken disposingToken = _disposingCts.Token; | |
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(disposingToken, stoppingToken); | |
BackgroundTask = Task.Run(TimerDelegate(new(this), firstTcs, () => factory(disposingToken), period, errorHandling, _stoppingCts.Token), disposingToken); | |
static Func<Task> TimerDelegate(WeakReference<Later<T>> thisRef, TaskCompletionSource<T> firstTcs_, Func<Task<T>> factory, TimeSpan period, LaterErrorHandling errorHandling, CancellationToken stoppingToken) => async () => { | |
TaskCompletionSource<T>? firstTcs = firstTcs_; | |
try { | |
do { | |
var periodTask = Task.Delay(period, stoppingToken); | |
Task<T>? valueTask = null; | |
try { | |
valueTask = factory(); | |
T value = await valueTask.ConfigureAwait(false); | |
firstTcs?.TrySetResult(value); | |
} | |
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { | |
break; | |
} | |
catch (Exception ex) { | |
switch (errorHandling) { | |
case LaterErrorHandling.Capture: | |
default: | |
valueTask ??= Task.FromException<T>(ex); | |
firstTcs?.TrySetException(ex); | |
break; | |
case LaterErrorHandling.Ignore: | |
goto Next; | |
} | |
} | |
firstTcs = null; | |
if (!thisRef.TryGetTarget(out Later<T>? @this)) { break; } | |
@this._valueTask = valueTask; | |
@this = null; // NOTE: 参照外し | |
Next: | |
await periodTask.ConfigureAwait(false); | |
} while (!stoppingToken.IsCancellationRequested); | |
} | |
catch (OperationCanceledException ex) when (ex.CancellationToken == stoppingToken) { } | |
}; | |
} | |
~Later() { | |
Dispose(false); | |
} | |
private int _disposed; | |
public void Dispose() { | |
Dispose(true); | |
GC.SuppressFinalize(this); | |
} | |
private void Dispose(bool disposing) { | |
if (Interlocked.Exchange(ref _disposed, 1) is 0) { | |
_disposingCts.Cancel(); | |
if (disposing) { | |
_disposingCts.Dispose(); | |
_stoppingCts.Dispose(); | |
_valueTask = null!; | |
} | |
} | |
} | |
private void ThrowIfDisposed() { | |
ObjectDisposedException.ThrowIf(_disposed is not 0, this); | |
} | |
/// <remarks> | |
/// いずれの方法で自動更新を止めても、 <see cref="Task.IsCompletedSuccessfully"/> になります。 | |
/// </remarks> | |
public Task BackgroundTask { get; } | |
/// <summary> | |
/// 値の自動更新を停止します。 | |
/// </summary> | |
public void Stop() { | |
ThrowIfDisposed(); | |
_stoppingCts.Cancel(); | |
} | |
public Task<T> GetValueAsync(CancellationToken cancellationToken = default) { | |
ThrowIfDisposed(); | |
return _valueTask.WaitAsync(cancellationToken); | |
} | |
} | |
public enum LaterErrorHandling { | |
/// <summary> | |
/// エラーを値とする。 | |
/// </summary> | |
Capture = 0, | |
/// <summary> | |
/// 値を更新しない。 | |
/// </summary> | |
Ignore = 1, | |
///// <summary> | |
///// 自動更新を停止する。 | |
///// </summary> | |
//Stop = 2, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
async Task Main() { | |
// Arrange | |
int count = 0; | |
using CancellationTokenSource cts = new(); | |
Later<int> later = new(ct => { | |
try { | |
return Task.FromResult(++count); | |
} | |
finally { | |
if (count is 3) { | |
cts.Cancel(); | |
} | |
} | |
}, period: TimeSpan.Zero, cts.Token); | |
// Act | |
List<int> actual = new(); | |
actual.Add(await later.GetValueAsync()); | |
await later.BackgroundTask; | |
actual.Add(await later.GetValueAsync()); | |
// Assert | |
// [1,3] | |
actual.Dump(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment