Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9700

C#でAwaitableなタイマーを作成する

$
0
0

TaskCompletionSourceを利用して、System.Timers.Timerにインターバル時間の経過を待機できるタスクを追加しました。

開発・実行環境
Visual Studio 2019 Community
.Net Framework 4.7.2
C# 7.3

イベントベース非同期処理をタスクベース非同期に変換する

一定の時間間隔で何らかの処理を行いたいとき、タイマーを利用することが多々あります。
私はこのような目的でよくSystem.Timers.Timerを利用します。このタイマーは、Intervalプロパティで指定したインターバル時間毎にElapsedイベントを発行することで、利用者に時間経過を通知します。イベントベースで処理する場合、例えば一定回数だけイベントが発行されたらタイマーを停止して処理をやめる、といったことを行いたい場合、コードが煩雑になりがちで、また処理の流れに沿った直感的なコーディングは難しいです。そこで、TaskCompletionSourceを利用してイベントをタスクに変換する方法をつい最近(今更)知ったので、Elapsedイベントを待機できるタスクを追加したAwaitableTimerクラスを作成してみました。

AwaitableTimer.cs
usingSystem;usingSystem.Threading;usingSystem.Threading.Tasks;usingSystem.Timers;namespaceTimerSample{/// <summary>/// <see cref="System.Timers.Timer"/>のインターバル時間の経過イベントを待機できるタスクを提供します。/// </summary>publicclassAwaitableTimer:System.Timers.Timer{#regionfieldprivateTaskCompletionSource<DateTime>_tcs;#endregion#regionconstructor/// <summary>/// デフォルトコンストラクタ/// </summary>publicAwaitableTimer():base(){Elapsed+=OnElapsed;}/// <summary>/// インターバル時間、およびインターバル経過イベントを繰り返し発生させるかどうかを指定してタイマーを作成します。/// </summary>/// <param name="interval"></param>/// <param name="autoReset"></param>publicAwaitableTimer(TimeSpaninterval,boolautoReset):base(interval.TotalMilliseconds){AutoReset=autoReset;Elapsed+=OnElapsed;}/// <summary>/// インターバル時間を指定して初期化/// </summary>/// <param name="interval"></param>publicAwaitableTimer(TimeSpaninterval):base(interval.TotalMilliseconds){Elapsed+=OnElapsed;}#endregionprivatevoidOnElapsed(objectsender,ElapsedEventArgse){if(_tcs!=null){_tcs.TrySetResult(e.SignalTime);_tcs=null;}}privatevoidOnTaskCanceled(){if(_tcs!=null){_tcs.TrySetException(newTaskCanceledException(_tcs.Task));_tcs=null;}}/// <summary>/// インターバル時間とインターバル経過イベントを繰り返し発生させるかどうかを指定して、タイマーを開始します。/// </summary>/// <param name="interval"></param>/// <param name="autoReset"></param>/// <returns></returns>publicstaticAwaitableTimerStartNew(TimeSpaninterval,boolautoReset){vartimer=newAwaitableTimer(interval,autoReset);timer.Start();returntimer;}/// <summary>/// タイマーの経過時間をリセットして再開します/// </summary>publicvoidRestart(){Stop();Start();}/// <summary>/// インターバル経過イベントを待機するタスク。タイマーが止まっている場合は、即時に<see cref="DateTime.Now"/>を返します。/// </summary>/// <param name="token"></param>/// <returns></returns>publicasyncTask<DateTime>WaitElapsedAsync(CancellationTokentoken=default){if(Enabled){_tcs=newTaskCompletionSource<DateTime>(TaskCreationOptions.RunContinuationsAsynchronously);varregister=token.Register(OnTaskCanceled);using(register){returnawait_tcs.Task;}}else{returnDateTime.Now;}}}}

例えば以下のように使います。この例では指定した時間間隔で、指定した回数だけ何らかの処理(ここではAsyncMethod)を行います。何らかの理由でインターバル時間内に処理が終わっていなかったらタイムアウトとし、ループを抜け出します。時間内に処理が終わったら、次のインターバルを待ちます。

usingSystem;usingstaticSystem.Console;usingSystem.Threading.Tasks;namespaceTimerSample{classProgram{staticreadonlyRandomRandom=newRandom();staticasyncTaskMain(string[]args){while(true){if(ReadKey().Key==ConsoleKey.Escape)break;Clear();WriteLine("インターバル時間をミリ秒で指定");if(!double.TryParse(ReadLine(),outvarinterval))continue;WriteLine("繰り返し回数を指定");if(!int.TryParse(ReadLine(),outvarn))continue;WriteLine("不具合の起こる確率を指定");if(!double.TryParse(ReadLine(),outvarp))continue;booltimeout=false;boolisRunning=false;asyncTaskAsyncMethod(){intdelay=0;if(Random.NextDouble()>p){delay=(int)(0.5*interval);}else{delay=(int)(2*interval);}isRunning=true;awaitTask.Delay(delay);isRunning=false;}using(varintervalTimer=newAwaitableTimer(TimeSpan.FromMilliseconds(interval),true)){intervalTimer.Elapsed+=(s,e)=>{if(isRunning){timeout=true;}};intervalTimer.Start();varstartTime=DateTime.Now;varprevious=startTime;for(vari=0;i<n;i++){awaitAsyncMethod();if(timeout){WriteLine("インターバル時間内に処理が終わりませんでした!");timeout=false;break;}varnow=awaitintervalTimer.WaitElapsedAsync();varperiod=now-previous;previous=now;WriteLine($"ラップ{i+1:000} 経過時間(トータル):{(now-startTime).TotalMilliseconds:f2} ms 経過時間(インターバル):{period.TotalMilliseconds:f2} ms");}}WriteLine("終了");}}}}

Elapsedイベントを利用したタイムアウト判定と、async/awaitを利用したインターバル時間経過の待機を併用しています。全てイベントベースで書くよりもいくらか簡単かつ直感的になった気がします。

実行結果

インターバル時間をミリ秒で指定
1000
繰り返し回数を指定
20
不具合の起こる確率を指定
0.1
ラップ001 経過時間(トータル):1000.15 ms 経過時間(インターバル):1000.15 ms
ラップ002 経過時間(トータル):2000.73 ms 経過時間(インターバル):1000.57 ms
ラップ003 経過時間(トータル):3002.09 ms 経過時間(インターバル):1001.36 ms
ラップ004 経過時間(トータル):4002.23 ms 経過時間(インターバル):1000.14 ms
ラップ005 経過時間(トータル):5002.55 ms 経過時間(インターバル):1000.32 ms
インターバル時間内に処理が終わりませんでした!
終了

参考資料

タスクとイベント ベースの非同期パターン (EAP)


Viewing all articles
Browse latest Browse all 9700

Trending Articles