タイマにより一定時間間隔で処理を行うには?(スレッドタイマ編):.NET TIPS
System.Threading名前空間で提供されているTimerクラスを利用して、一定間隔で処理を行う方法を説明する。
本稿は2005/11/11に初版公開された記事を改訂し 、Visual Studio 2017でコードの動作検証、ラムダ式の使用例の追加、GUIアプリでの使用例の追加、図版の追加、全般的な構成の変更などを行ったものです。
.NET Frameworkには一定時間間隔で処理を行う(メソッドを呼び出す)ためのタイマ機能として、以下の3種類のTimerクラスが用意されている。
本稿では2のスレッドタイマについて、その基本的な使い方をまとめる。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
スレッドタイマ:System.Threading.Timerクラスの基本
System.Threading名前空間には、マルチスレッド機能の一つとしてTimerクラスが用意されている。このタイマは扱いが少々面倒だが、サーバベースタイマ(System.Timers.Timerクラス)などに比べて軽量である。また、Windowsタイマよりも精度が高く、Windows 7/8.xにおいて最小間隔は約15ミリ秒である(Windows 10でも同じはずであるが、公式ドキュメントに記載を見つけられなかった)。
Timerクラスを使うには、まずTimerCallbackデリゲート(System.Threading名前空間)を使用して、タイマにより一定間隔で呼び出したいメソッド(以下、タイマメソッドと記す)のデリゲートを作成する。
次にTimerクラスのコンストラクタで、このデリゲートと、タイマメソッドにパラメーターとして渡したい任意のオブジェクト、タイマメソッドが最初に呼び出されるまでの待機時間、タイマメソッドの呼び出し間隔(いずれも単位はミリ秒)を指定し、インスタンスを作成する。インスタンスの作成後は、指定した待機時間が経過するとタイマメソッドの呼び出しが開始される。
また、TimerクラスにはChangeメソッドが用意されており、このメソッドによりタイマメソッドが呼び出される間隔を変更できる。タイマを一時的に停止させるにはChangeメソッドでタイマの待機時間としてTimeout.Infinite(System.Threading名前空間のTimeoutクラスのInfiniteフィールド)あるいは-1を指定すればよい。再開するときは、Changeメソッドで待機時間と呼び出し間隔を再び設定し直す。なお、Timerオブジェクトが保持しているリソースを確実に破棄するには、Disposeメソッドを呼び出す。
以下にスレッドタイマを利用したサンプルプログラムを示す。MyClockメソッドがタイマにより一定間隔で実行されるメソッドである。Visual StudioでVBのコンソールアプリプロジェクトを新規に作成して、以下のコードを試す場合には、ソリューションエクスプローラーの[My Project]をダブルクリックして、[アプリケーション]タブにある[スタートアップ オブジェクト]に[Sub Main]か[ThreadTimerTest]に変更する必要がある。
// threadtimer.cs
using System;
using System.Threading;
public class ThreadTimerTest {
static void Main() {
ThreadTimerTest ttt = new ThreadTimerTest();
ttt.Run();
}
public void Run() {
TimerCallback timerDelegate = new TimerCallback(MyClock);
Timer timer = new Timer(timerDelegate, null , 0, 1000);
Console.ReadLine(); // Enterキーが押されるまで待機
timer.Change(Timeout.Infinite, Timeout.Infinite);
Console.WriteLine("タイマー停止");
timer.Dispose();
}
public void MyClock(object o) {
Console.WriteLine(DateTime.Now);
// 出力例:
// 2005/11/08 19:59:10
// 2005/11/08 19:59:11
// 2005/11/08 19:59:12
// ……
}
}
// コンパイル方法:csc threadtimer.cs
' threadtimer.vb
Imports System
Imports System.Threading
Public Class ThreadTimerTest
Shared Sub Main()
Dim ttt As ThreadTimerTest = New ThreadTimerTest()
ttt.Run()
End Sub
Public Sub Run()
Dim timerDelegate As TimerCallback _
= New TimerCallback(AddressOf MyClock)
Dim timer As Timer _
= New Timer(timerDelegate, Nothing, 0, 1000)
Console.ReadLine() ' Enterキーが押されるまで待機
timer.Change(Timeout.Infinite, Timeout.Infinite)
Console.WriteLine("タイマー停止")
timer.Dispose()
End Sub
Public Sub MyClock(o As Object)
Console.WriteLine(DateTime.Now)
' 出力例:
' 2005/11/08 19:59:10
' 2005/11/08 19:59:11
' 2005/11/08 19:59:12
' ……
End Sub
End Class
' コンパイル方法:vbc threadtimer.vb
ラムダ式で簡潔に記述する
.NET Framework 3.5からは、デリゲートに替えてラムダ式が使える。タイマメソッドの内容がさほど長くないときは、ラムダ式にすると簡潔に記述できる。上のコードをラムダ式を使って書き直すと、次のコードのようになる。
using System;
using System.Threading;
class Program
{
static void Main(string[] args)
{
// TimerCallbackをラムダ式で定義
TimerCallback timerCallback = state =>
{
Console.WriteLine(DateTime.Now);
};
var timer = new Timer(timerCallback, null, 0, 1000);
Console.ReadKey(); // キーが押されるまで待機
timer.Change(Timeout.Infinite, Timeout.Infinite);
Console.WriteLine("タイマー停止");
timer.Dispose();
}
}
Imports System.Threading
Module Module1
Sub Main()
' TimerCallbackをラムダ式で定義
Dim timerCallback As TimerCallback _
= Sub(state)
Console.WriteLine(DateTime.Now)
End Sub
Dim timer = New Timer(timerCallback, Nothing, 0, 1000)
Console.ReadKey() ' キーが押されるまで待機
timer.Change(Timeout.Infinite, Timeout.Infinite)
Console.WriteLine("タイマー停止")
timer.Dispose()
End Sub
End Module
この例と前の例では、明示的にDisposeメソッドを呼び出す代わりにusing句を使ってもよい。実際には、次に示すWPFの例のように、Timerクラスのインスタンス化と破棄は別の場所になることが多い(=using句が使えない)。
GUIアプリで使う場合
タイマメソッドは.NET Frameworkが管理するスレッドプールにキューイングされて実行されるため、タイマメソッドはTimerクラスをインスタンス化したスレッドとは異なるスレッドで実行されることになる。
このためGUIアプリで使用する場合には、タイマメソッドからのコントロールの操作に関して注意が必要となる。Windowsフォームの場合は、「TIPS:Windowsフォームで別スレッドからコントロールを操作するには?」を参照していただきたい。ここでは、WPFの場合を紹介する。
WPFのUIコントロールはDispatcherプロパティを持っている。その実体はDispatcherクラス(System.Windows.Threading名前空間)のインスタンスだ。Dispatcherクラスはインスタンス化されたときのスレッドで(すなわちUIスレッドで)、与えられたデリゲートを実行する。
Dispatcherクラスのデリゲートを実行するメソッドには、InvokeメソッドとBeginInvokeメソッド、それに.NET Framework 4.5で追加されたInvokeAsyncメソッドの3種類がある。Invokeメソッドは、デリゲートの実行が完了してから制御が返ってくる。BeginInvokeメソッドは、直ちに制御が返ってきて、その後で非同期的にデリゲートが実行される。InvokeAsyncメソッドはBeginInvokeメソッドと似ているが、デリゲート内で発生した例外をtry〜catchできる(awaitした場合)。
実際の例を見てみよう。まず、画面には次のコードのようにして、TextBlockコントロールとToggleButtonコントロールを配置しておく。
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="textBlock" ……省略…… />
<ToggleButton x:Name="toggleButton"
Checked="toggleButton_Checked"
Unchecked="toggleButton_Unchecked"
Grid.Row="1" ……省略…… />
</Grid>
省略した部分では、コントロールの位置やサイズ、フォントや色などを指定している。
コードビハインドは次のコードのようになる。InitializeComponentメソッド呼び出しの後で、Timerオブジェクトを停止状態で生成しておく。ボタンのトグル操作で、タイマの動作を開始/停止する。タイマから呼び出されるMyTimerCallbackメソッドでは、画面が持っているDispatcherインスタンスを使ってデリゲートを実行している。デリゲートは、ラムダ式で記述したActionデリゲートになっている。
using System;
using System.Threading;
using System.Windows;
namespace dotNetTips0372WpfCS
{
public partial class MainWindow : Window
{
Timer _timer;
// タイマから呼び出されるメソッド
void MyTimerCallback(object state)
{
// WPFではDispatcherを使ってUIスレッドでの処理を実行する
this.Dispatcher.Invoke(new Action(() => {
this.textBlock.Text = DateTime.Now.ToString("HH:mm:ss");
}));
}
public MainWindow()
{
InitializeComponent();
// タイマの生成
_timer = new Timer(MyTimerCallback, null,
Timeout.Infinite, Timeout.Infinite);
this.Closing += (s, e) => {
// 画面が閉じられるときに、タイマを停止して破棄
_timer.Change(Timeout.Infinite, Timeout.Infinite);
_timer.Dispose();
};
// タイマ開始ボタンをONに(=タイマがスタートする)
this.toggleButton.IsChecked = true;
}
private void toggleButton_Checked(object sender, RoutedEventArgs e)
{
_timer.Change(0, 1000); // タイマ開始
toggleButton.Content = "STOP";
}
private void toggleButton_Unchecked(object sender, RoutedEventArgs e)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite); // タイマ停止
toggleButton.Content = "RUN";
}
}
}
Imports System.Threading
Class MainWindow
Private _timer As Timer
' タイマから呼び出されるメソッド
Sub MyTimerCallback(state As Object)
' WPFではDispatcherを使ってUIスレッドでの処理を実行する
Me.Dispatcher.Invoke(New Action(
Sub()
Me.textBlock.Text = DateTime.Now.ToString("HH:mm:ss")
End Sub))
End Sub
Public Sub New()
InitializeComponent()
' タイマの生成
_timer = New Timer(AddressOf MyTimerCallback, Nothing, _
Timeout.Infinite, Timeout.Infinite)
AddHandler Me.Closing,
Sub(s, e)
' 画面が閉じられるときに、タイマを停止して破棄
_timer.Change(Timeout.Infinite, Timeout.Infinite)
_timer.Dispose()
End Sub
' タイマ開始ボタンをONに(=タイマがスタートする)
Me.toggleButton.IsChecked = True
End Sub
Private Sub toggleButton_Checked(sender As Object, e As RoutedEventArgs)
_timer.Change(0, 1000) ' タイマ開始
toggleButton.Content = "STOP"
End Sub
Private Sub toggleButton_Unchecked(sender As Object, e As RoutedEventArgs)
_timer.Change(Timeout.Infinite, Timeout.Infinite) ' タイマ停止
toggleButton.Content = "RUN"
End Sub
End Class
C#のnamespace宣言「dotNetTips0372WpfCS」は、適切な名前に変更してほしい。
ここでは画面が保持しているDispatcherオブジェクト(=this.Dispatcher)を使っているが、その他のコントロールのもの(例えばthis.textBlock.Dispatcherなど)を使っても同じである。
なお、MyTimerCallbackメソッドはTimerCallbackデリゲートとして使うだけなので、タイマを生成する場所にラムダ式として記述してもよい。ただそうすると、ラムダ式の中にラムダ式(Actionデリゲート)が入ることになって少し分かりづらくなるので、あえてMyTimerCallbackメソッドとして独立させた。
実行してみると次の画像のようになる。1秒ごとに時刻が書き換わる。
カテゴリ:クラスライブラリ 処理対象:スレッド
カテゴリ:クラスライブラリ 処理対象:タイマ
使用ライブラリ:Timerクラス(System.Threading名前空間)
使用ライブラリ:TimerCallbackデリゲート(System.Threading名前空間)
使用ライブラリ:Timeoutクラス(System.Threading名前空間)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(Windowsタイマ編)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(サーバベース・タイマ編)
関連TIPS:Windowsフォームで別スレッドからコントロールを操作するには?
更新履歴
【2018/11/14】Visual Studio 2017でコードの動作検証、ラムダ式の使用例の追加、GUIアプリでの使用例の追加、図版の追加、全般的な構成の変更などを行いました。
【2005/11/11】初版公開。
Copyright© Digital Advantage Corp. All Rights Reserved.