Skip to content

Instantly share code, notes, and snippets.

@in-async
Last active October 13, 2023 11:29
Show Gist options
  • Save in-async/b5443d49c4563c52919d027bdf8b20f3 to your computer and use it in GitHub Desktop.
Save in-async/b5443d49c4563c52919d027bdf8b20f3 to your computer and use it in GitHub Desktop.
定期的に自動更新される値を表すクラス。
/// <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,
}
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