はじめに
ここ半年ほどC#を使ってwindowsアプリを開発しています。
馴染みのない言語のため、日々勉強中です。
学習ログも兼ねつつ、
今後にも活かせるようにいくつかモジュール化できたらいいかもなーと考え、
検討の過程やコードを残せる範囲で残したいと思います。
今回のお題
ある処理の終了条件ですね。
クラスA
から クラスB
に処理を委譲、非同期で待ちます。
この時、一定時間が経過した場合 で処理を分けたい。
タイムアウト機能を実装したいという状況です。
以下のようなイメージ。
設計
- Copilotに相談しつつ設計案と実装を進める
- .NETの機能である TaskCompletionSource を使うのが良さそう
- いくつかバリエーションがありますが、今回はクラスとして独立させておき、単一のクラス内で使いまわせるようにする
- 異なるメソッドから、タイマーの開始と完了を呼び出せる
- インスタンスを共有すれば、他クラスからも呼び出しok
- キャンセル機能は、オプションでさらに改善できる模様
実装(サンプルコード)
こんなイメージ。
Testerがタイムアウトの開始やら完了やらする。
Program.cs
class Program
{
static void Main(string[] args)
{
var tester = new Tester();
tester.SuccessRun();
tester.TimeoutRun();
tester.TimerCheck();
}
}
Tester.cs
public class Tester
{
private readonly TimeoutManager _timeoutManager = new TimeoutManager();
public void SuccessRun()
{
// 3000msのタイムアウトを設定する
string key = "task1";
int timeoutMs = 3000;
// 1000msで処理完了しタイマー解除する
Task.Run(async () =>
{
await Task.Delay(1000);
_timeoutManager.CompleteTimer(key);
});
bool completed = _timeoutManager.StartTimer(key, timeoutMs).GetAwaiter().GetResult();
if (completed)
{
Console.WriteLine("正常に完了しました");
}
else
{
Console.WriteLine("タイムアウトしました");
}
}
public void TimeoutRun()
{
// 3000msのタイムアウトを設定する
string key = "task2";
int timeoutMs = 3000;
// 5000msで処理完了しタイマー解除する
Task.Run(async () =>
{
await Task.Delay(5000);
_timeoutManager.CompleteTimer(key);
});
bool completed = _timeoutManager.StartTimer(key, timeoutMs).GetAwaiter().GetResult();
if (completed)
{
Console.WriteLine("正常に完了しました");
}
else
{
Console.WriteLine("タイムアウトしました");
}
}
public void TimerCheck()
{
_timeoutManager.CompleteTimer("task3");
bool completed1 = _timeoutManager.StartTimer("task4", 3000).GetAwaiter().GetResult();
bool completed2 = _timeoutManager.StartTimer("task4", 3000).GetAwaiter().GetResult();
}
}
TimeoutManager.cs
/// <summary>
/// タイムアウトを管理するクラス
/// </summary>
public class TimeoutManager
{
// マルチスレッドに対応した辞書
private readonly ConcurrentDictionary<string, TaskCompletionSource<bool>> _timers = new ConcurrentDictionary<string, TaskCompletionSource<bool>>();
public async Task<bool> StartTimer(string key, int timeoutMs)
{
// 重複チェックは仕様次第、今回は許容しない
if (_timers.ContainsKey(key))
{
Console.WriteLine($"タイマー {key} は既に存在します");
return false;
}
// オプションを指定しデッドロック防止する
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
_timers[key] = tcs;
// タイムアウト時間を計測するタスクを生成
// 完了通知 と タイムアウト のどちらかが完了するまで待機
var timeoutTask = Task.Delay(timeoutMs);
var completedTask = await Task.WhenAny(tcs.Task, timeoutTask).ConfigureAwait(false);
if (completedTask == tcs.Task)
{
_timers.TryRemove(key, out _);
return true;
}
return false;
}
public void CompleteTimer(string key)
{
if (!_timers.TryRemove(key, out var tcs))
{
Console.WriteLine($"タイマー {key} はありません");
return;
}
tcs.TrySetResult(true);
}
}