スレッドタイマ
.NET Frameworkで用意されているタイマには、次の3種類があります。
System.Windows.Forms.Timer
(Windowsタイマ)System.Threading.Timer
(スレッドタイマ)System.Timers.Timer
(サーバータイマ)
最も馴染み深いタイマは、Windowsタイマでしょう。Windowsアプリケーションプロジェクトのフォームデザイナで[ツールボックス]の[Windowsフォーム]内に[Timer]としてあるのが、Windowsタイマです。最もよく使われるタイマですが、OSのタイマ機能を使用しているため、スレッドでメッセージを処理しなければタイマイベントは発生せず、長い処理によりブロックされる恐れがあります。これに対してサーバータイマとスレッドタイマはワーカースレッドにより処理されますので、その心配がありません。
フォームデザイナで[ツールボックス]の[コンポーネント]内にある[Timer]がサーバータイマです。パート2のスレッドプールの解説で触れたとおり、サーバータイマはスレッドプールを使用します。なお、サーバータイマについてMSDNライブラリの「サーバー ベースのタイマの概説」には、「サーバーベースのタイマは、サーバー環境での実行用に最適化された従来のタイマを強化したものです」とあります。
スレッドタイマもスレッドプールを使用したタイマです。イベントの代わりにコールバックメソッドを使用します。軽量で、簡単なタイマ処理が必要な時に便利です。
サンプル「timer_01」でスレッドタイマの使用例を示します。
Class Class1 'エントリポイント Public Shared Sub Main() 'Threading.Timerオブジェクトの作成 'TimerState.Tickを0.1秒後に1秒間隔で実行する Dim tmr As New System.Threading.Timer( _ New System.Threading.TimerCallback(AddressOf Tick), _ Nothing, 100, 1000) System.Threading.Thread.Sleep(5300) 'タイマの開始時間とコールバックメソッドの呼び出し間隔を変更 tmr.Change(500, 500) Console.WriteLine("Changed") System.Threading.Thread.Sleep(5300) 'コールバックメソッドの呼び出しを停止 tmr.Change(System.Threading.Timeout.Infinite, _ System.Threading.Timeout.Infinite) Console.WriteLine("Stop") System.Threading.Thread.Sleep(5300) 'タイマを破棄 tmr.Dispose() Console.WriteLine("Disposed") '待機する Console.ReadLine() End Sub Public Shared Sub Tick(ByVal state As Object) '表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _ System.Environment.TickCount) End Sub End Class
class Class1 { //エントリポイント public static void Main() { //Threading.Timerオブジェクトの作成 //TimerState.Tickを0.1秒後に1秒間隔で実行する System.Threading.Timer tmr = new System.Threading.Timer( new System.Threading.TimerCallback(Tick), null, 100, 1000); System.Threading.Thread.Sleep(5300); //タイマの開始時間とコールバックメソッドの呼び出し間隔を変更 tmr.Change(500, 500); Console.WriteLine("Changed"); System.Threading.Thread.Sleep(5300); //コールバックメソッドの呼び出しを停止 tmr.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); Console.WriteLine("Stop"); System.Threading.Thread.Sleep(5300); //タイマを破棄 tmr.Dispose(); Console.WriteLine("Disposed"); //待機する Console.ReadLine(); } public static void Tick(object state) { //表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", System.Environment.TickCount); } }
スレッドタイマを作成するには、インスタンス作成時のコンストラクタで実行するメソッドへのTimerCallback
デリゲート、メソッドで使用される状態オブジェクト、最初に発生する時間、発生する時間間隔を渡します。最初に発生する時間、発生する時間間隔を変更するには、Change
メソッドを使います。タイマを破棄するには、Dispose
メソッドを呼び出します。タイマの破棄は必要です。
タイマを破棄するのではなく、一時的に停止させたい場合は、Change
メソッドで開始時間をTimeout.Infinite
にするという方法があります。また、コールバックメソッドを一回だけ実行したいときは、呼び出し間隔をTimeout.Infinite
にするという方法があります。
サンプル「timer_01」では、Tick
メソッドをスレッドタイマにより定期的に実行します。まず0.1秒後に1秒間隔でTick
メソッドが呼び出されるようにしています。その後5秒後に、Change
メソッドで開始時間と間隔を0.5秒に変更します。さらにその5秒後に開始時間をTimeout.Infinite
にすることによりタイマを一時停止し、その5秒後にタイマを破棄しています。
スレッドタイマを使用する手順をまとめると、次のようになります。
- タイマで定期的に呼び出すメソッドとして、
TimerCallback
デリゲートと同じシグネチャのメソッドを作成する。 TimerCallback
デリゲートオブジェクト、状態オブジェクト、開始時間、時間間隔を指定して、Timer
オブジェクトを作成する。- 開始時間と間隔を変更するときは、
Change
メソッドを呼び出す。 - タイマが不要になったときに
Dispose
メソッドを呼び出す。
ThreadPool.RegisterWaitForSingleObjectメソッド
ThreadPool.QueueUserWorkItem
メソッドよりも、より多くの機能を持つメソッドがThreadPool.RegisterWaitForSingleObject
メソッドです。QueueUserWorkItem
メソッドと比較してRegisterWaitForSingleObject
メソッドの特徴は、WaitHandle
を使って実行を待機できる点と、タイムアウトを指定できる点にあります。
WaitHandleによる待機
WaitHandle
オブジェクトを使用した例が、サンプル「threadpool2_01」です。
Class Class1 'エントリポイント Public Shared Sub Main() '非シグナル状態でManualResetEventを作成 Dim ev As New System.Threading.ManualResetEvent(False) Dim i As Integer For i = 0 To 9 'デリゲートをキューに追加する System.Threading.ThreadPool.RegisterWaitForSingleObject( _ ev, _ New System.Threading.WaitOrTimerCallback( _ AddressOf WriteTime), _ Nothing, _ -1, _ True) Next i '待機する Console.WriteLine("Enterキーで開始") Console.ReadLine() 'ManualResetEventをシグナル状態にする ev.Set() Console.ReadLine() End Sub 'システム起動後の経過時間を表示 Private Shared Sub WriteTime(ByVal state As Object, _ ByVal timedOut As Boolean) Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _ System.Environment.TickCount) End Sub End Class
class Class1 { //エントリポイント public static void Main() { //非シグナル状態でManualResetEventを作成 System.Threading.ManualResetEvent ev = new System.Threading.ManualResetEvent(false); for (int i = 0; i < 10; i++) { //デリゲートをキューに追加する System.Threading.ThreadPool.RegisterWaitForSingleObject( ev, new System.Threading.WaitOrTimerCallback(WriteTime), null, -1, true ); } //待機する Console.WriteLine("Enterキーで開始"); Console.ReadLine(); //ManualResetEventをシグナル状態にする ev.Set(); Console.ReadLine(); } //システム起動後の経過時間を表示 private static void WriteTime(object state, bool timedOut) { Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", System.Environment.TickCount); } }
RegisterWaitForSingleObject
メソッドによりWriteTime
メソッドが10回スレッドプールのキューに追加されますが、WaitHandle
オブジェクトのev
が非シグナル状態のため、すぐには実行されません。Set
メソッドによりev
がシグナル状態になると待機が解除され、ワーカースレッドにより一斉に実行されるようになります。
タイムアウト
サンプル「threadpool2_02」では、さらにタイムアウトを使用した例を紹介しています。
Class Class1 'エントリポイント Public Shared Sub Main() '非シグナル状態でAutoResetEventを作成 Dim ev As New System.Threading.AutoResetEvent(False) 'デリゲートをキューに追加する '1秒おきに実行する System.Threading.ThreadPool.RegisterWaitForSingleObject( _ ev, _ New System.Threading.WaitOrTimerCallback( _ AddressOf WriteTime), _ Nothing, _ 1000, _ False) Dim i As Integer For i = 0 To 9 '3秒待機する System.Threading.Thread.Sleep(3000) 'AutoResetEventをシグナル状態にする ev.Set() Next i Console.ReadLine() End Sub 'システム起動後の経過時間を表示 Private Shared Sub WriteTime(ByVal state As Object, _ ByVal timedOut As Boolean) Console.WriteLine( _ "システム起動後の経過時間:{0}ミリ秒 / タイムアウト:{1}", _ System.Environment.TickCount, timedOut) End Sub End Class
class Class1 { //エントリポイント public static void Main() { //非シグナル状態でAutoResetEventを作成 System.Threading.AutoResetEvent ev = new System.Threading.AutoResetEvent(false); //デリゲートをキューに追加する //1秒おきに実行する System.Threading.ThreadPool.RegisterWaitForSingleObject( ev, new System.Threading.WaitOrTimerCallback(WriteTime), null, 1000, false ); for (int i = 0; i < 10; i++) { //3秒待機する System.Threading.Thread.Sleep(3000); //AutoResetEventをシグナル状態にする ev.Set(); } Console.ReadLine(); } //システム起動後の経過時間を表示 private static void WriteTime(object state, bool timedOut) { Console.WriteLine( "システム起動後の経過時間:{0}ミリ秒 / タイムアウト:{1}", System.Environment.TickCount, timedOut); } }
このプログラムを実行すると、例えば次のように出力されます。ほぼ1秒ごとに「タイムアウト」が「True」となっている出力と、3秒ごとに「タイムアウト」が「False」となっている出力が続いていることが分かります。
システム起動後の経過時間:13193601ミリ秒 / タイムアウト:True システム起動後の経過時間:13194592ミリ秒 / タイムアウト:True システム起動後の経過時間:13195594ミリ秒 / タイムアウト:False システム起動後の経過時間:13196595ミリ秒 / タイムアウト:True システム起動後の経過時間:13197597ミリ秒 / タイムアウト:True システム起動後の経過時間:13198598ミリ秒 / タイムアウト:True システム起動後の経過時間:13198598ミリ秒 / タイムアウト:False システム起動後の経過時間:13199600ミリ秒 / タイムアウト:True システム起動後の経過時間:13200591ミリ秒 / タイムアウト:True システム起動後の経過時間:13201592ミリ秒 / タイムアウト:True システム起動後の経過時間:13201592ミリ秒 / タイムアウト:False システム起動後の経過時間:13202594ミリ秒 / タイムアウト:True (以下省略)
ここでは、タイムアウトする時間を4番目の引数に指定してRegisterWaitForSingleObject
メソッドを呼び出しています。このときは、WaitHandle
がシグナル状態にならなくても、タイムアウト間隔が経過するとWriteTime
メソッドが実行されます。
また、RegisterWaitForSingleObject
メソッドの5番目の引数をfalse
にして、WriteTime
メソッドが何回も実行されるようにしています。このようにタイムアウトと併せて使用することにより、タイムアウトの時間が経過するたびに何回もデリゲートが実行され、タイマのような働きをします。
WriteTime
メソッドがシグナル通知により実行されたか、タイムアウトにより実行されたかは、WriteTime
メソッドのtimedOut
引数から分かります。タイムアウトにより実行された時はtrue
となります。
Unregisterメソッド
WaitHandle
がシグナル状態になるまで、またはタイムアウトするまで実行を待機するという待機操作は、RegisteredWaitHandle.Unregister
メソッドによりキャンセルすることができます。その例をサンプル「threadpool2_03」で示します。
'状態オブジェクトのためのクラス Class TaskInfo '待機操作のためのRegisteredWaitHandle Public Handle As System.Threading.RegisteredWaitHandle = Nothing 'メソッドに渡すデータ Public StringValue As String Public Sub New(ByVal str As String) StringValue = str End Sub End Class Class Class1 'エントリポイント Public Shared Sub Main() 'TaskInfoオブジェクトの作成 Dim ti As New TaskInfo("こんにちは") '非シグナル状態でAutoResetEventを作成 Dim ev As New System.Threading.AutoResetEvent(False) 'デリゲートをキューに追加する '状態オブジェクトを指定して、1秒おきに実行する ti.Handle = _ System.Threading.ThreadPool.RegisterWaitForSingleObject( _ ev, _ New System.Threading.WaitOrTimerCallback( _ AddressOf WriteTime), _ ti, _ 1000, _ False) '待機 Console.ReadLine() 'AutoResetEventをシグナル状態にする ev.Set() Console.ReadLine() End Sub 'システム起動後の経過時間を表示 Private Shared Sub WriteTime(ByVal state As Object, _ ByVal timedOut As Boolean) '状態オブジェクトの取得 Dim ti As TaskInfo = CType(state, TaskInfo) '表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _ System.Environment.TickCount) 'シグナルが送信されたら 'コールバックメソッドが実行されないように '待機操作をキャンセルする If Not timedOut AndAlso Not (ti.Handle Is Nothing) Then ti.Handle.Unregister(Nothing) End If End Sub End Class
//状態オブジェクトのためのクラス class TaskInfo { //待機操作のためのRegisteredWaitHandle public System.Threading.RegisteredWaitHandle Handle = null; //メソッドに渡すデータ public string StringValue; public TaskInfo(string str) { StringValue = str; } } class Class1 { //エントリポイント public static void Main() { //TaskInfoオブジェクトの作成 TaskInfo ti = new TaskInfo("こんにちは"); //非シグナル状態でAutoResetEventを作成 System.Threading.AutoResetEvent ev = new System.Threading.AutoResetEvent(false); //デリゲートをキューに追加する //状態オブジェクトを指定して、1秒おきに実行する ti.Handle = System.Threading.ThreadPool.RegisterWaitForSingleObject( ev, new System.Threading.WaitOrTimerCallback(WriteTime), ti, 1000, false ); //待機 Console.ReadLine(); //AutoResetEventをシグナル状態にする ev.Set(); Console.ReadLine(); } //システム起動後の経過時間を表示 private static void WriteTime(object state, bool timedOut) { //状態オブジェクトの取得 TaskInfo ti = (TaskInfo) state; //表示 Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", System.Environment.TickCount); //シグナルが送信されたら //コールバックメソッドが実行されないように //待機操作をキャンセルする if (!timedOut && ti.Handle != null) ti.Handle.Unregister(null); } }
この例では1秒おきにWriteTime
メソッドが実行されますが、AutoResetEvent.Set
メソッドによりシグナルを送ってWriteTime
メソッドを実行すると、「Unregister(null)
」により、それ以上は実行されなくなります。
まとめ
パート3では、Mutexによるプロセス間の同期や、スレッドタイマの利用方法などについて学びました。次回(パート4)は、スレッドの優先順位を変更したり、スレッドを強制終了・一時停止したりする方法について解説します。
参考資料
- Jon's Homepage! 『Multi-threading in .NET』
- MSDNライブラリ 『.NET Framework開発者ガイド-スレッド処理』
- MSDNライブラリ 『Visual Basic言語の概念-Visual Basic .NETにおけるマルチスレッド』
- MSDNライブラリ 『.NET Framework開発者ガイド-非同期呼び出しの組み込み』
- MSDNライブラリ 『コンポーネントのマルチスレッド』
- MSDNライブラリ 『スレッド処理のデザイン ガイドライン』
- MSDNライブラリ 『非同期プログラミングのガイドライン』
- MSDNライブラリ 『Chapter 4 - Architecture and Design Review of a .NET Application for Performance and Scalability』