連載
» 2018年12月05日 05時00分 公開

.NET TIPS:タイマにより一定時間間隔で処理を行うには?(サーバベースタイマ編)

System.Timers名前空間で提供されているTimerクラスを利用して、一定間隔で処理を実行する方法を解説する。

[遠藤孝信, 山本康彦,共著]
「.NET TIPS」のインデックス

連載「.NET TIPS」

本稿は2005/11/11に初版公開された記事を改訂し、Visual Studio 2017でコードの動作検証、ラムダ式の使用例、WPFアプリでの使用例、図版の追加、全般的な構成の変更などを行ったものです。


 .NET Frameworkには一定時間間隔で処理を行う(メソッドを呼び出す)ためのタイマ機能として、以下の3種類のTimerクラスが用意されている。

  1. Windowsタイマ:System.Windows.Forms.Timerクラス(解説TIPSへ
  2. スレッドタイマ:System.Threading.Timerクラス(解説TIPSへ
  3. サーバベースタイマ:System.Timers.Timerクラス

 本稿では3のサーバベースタイマについて、その基本的な使い方をまとめる。

POINT サーバベースタイマの使い方

サーバベースタイマの使い方まとめ サーバベースタイマの使い方まとめ


 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

サーバベースタイマ:System.Timers.Timerクラスの基本

 System.Timers名前空間のTimerクラスはサーバベースタイマとも呼ばれ、Windowsアプリだけでなくサーバサイドアプリでの利用も想定されており、より正確な時間間隔でメソッドを実行できる。

 このTimerクラスでは、ElapsedEventHandlerデリゲート(System.Timers名前空間)を使用して、タイマにより呼び出されるメソッド(以下、タイマメソッドと記す)のデリゲートを作成し、TimerクラスのElapsedイベント(elapseは「時が経過する」の意味)に登録する(VB.NETではWithEvents/Handlesキーワードによりイベントを登録することも可能)。

 タイマメソッドの呼び出し間隔は、コンストラクタのパラメーターかIntervalプロパティで指定できる(単位はミリ秒)。AutoResetプロパティでは、タイマメソッドを継続して呼び出すか(trueに設定。デフォルト値)、1度だけ呼び出すか(falseに設定)を指定する。また、タイマの開始/停止は、Enabledプロパティにtrue/falseを設定して行う(Start/Stopメソッドを呼び出してもよい)。

 以下にサーバベースタイマを利用したサンプルプログラムを示す。MyClockメソッドがタイマにより一定間隔で実行されるメソッドである。Visual StudioでVBのコンソールアプリプロジェクトを新規に作成して、以下のコードを試す場合には、ソリューションエクスプローラーの[My Project]をダブルクリックして、[アプリケーション]タブにある[スタートアップ オブジェクト]に[Sub Main]か[TimersTimerTest]に変更する必要がある。

// timerstimer.cs

using System;
using System.Timers;

public class TimersTimerTest {
  static void Main() {
    TimersTimerTest ttt = new TimersTimerTest();
    ttt.Run();
  }

  public void Run() {
    Timer timer = new Timer();
    timer.Elapsed += new ElapsedEventHandler(MyClock);
    timer.Interval  = 1000; // コンストラクタでも指定可
    timer.AutoReset = true; // デフォルト
    timer.Enabled   = true; // timer.Start()と同じ

    Console.ReadLine(); // Enterキーが押されるまで待機

    timer.Enabled = false; // timer.Stop()と同じ
    Console.WriteLine("タイマ停止");

    timer.Dispose();
  }

  public void MyClock(object sender, ElapsedEventArgs e) {
    Console.WriteLine(DateTime.Now);
    // 出力例:
    // 2005/11/08 19:59:10
    // 2005/11/08 19:59:11
    // 2005/11/08 19:59:12
    // ……
  }
}

// コンパイル方法:csc timerstimer.cs

' timerstimer.vb

Imports System
Imports System.Timers

Public Class TimersTimerTest
  Shared Sub Main()
    Dim ttt As TimersTimerTest = New TimersTimerTest()
    ttt.Run()
  End Sub

  Public Sub Run()
    Dim timer As Timer = New Timer()
    AddHandler timer.Elapsed, _
      New ElapsedEventHandler(AddressOf MyClock)
    timer.Interval = 1000 ' コンストラクタでも指定可
    timer.AutoReset = true ' デフォルト
    timer.Enabled  = true ' timer.Start()と同じ

    Console.ReadLine() ' Enterキーが押されるまで待機

    timer.Enabled = False ' timer.Stop()と同じ
    Console.WriteLine("タイマ停止")
  End Sub

  Public Sub MyClock(sender As Object, e As ElapsedEventArgs)
    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 timerstimer.vb

サーバベースタイマを使用したサンプルプログラム(上:C#/下:VB)
C#版サンプルプログラムのダウンロード
VB版サンプルプログラムのダウンロード

 サーバベースタイマはWindowsフォーム用のTimerコンポーネントと異なり、Webフォームなどでも利用可能だ。ちなみにこのタイマは、Windows OSの「待機可能タイマ」と呼ばれるタイマをベースにしている(Win32 APIではCreateWaitableTimer関数により待機可能タイマを作成できる)。

 なお、タイマメソッドを継続して呼び出す場合、呼び出し間隔よりもタイマメソッドの処理にかかる時間が長くなっても、タイマメソッドは呼び出される。タイマメソッドの処理中に重複してタイマメソッドが呼び出されることになるので、注意してほしい。そのような可能性があるときは、タイマメソッドをリエントラント(再入可能)に作る必要がある。

ラムダ式で簡潔に記述する

 .NET Framework 3.5からは、デリゲートに替えてラムダ式が使える。タイマメソッドの内容がさほど長くないときは、ラムダ式にすると簡潔に記述できる。上のコードをラムダ式を使って書き直すと、次のコードのようになる。

using System;
using System.Timers;

class Program
{
  static void Main(string[] args)
  {
    Timer timer = new Timer(1000); // コンストラクタでIntervalを指定
    // ElapsedEventHandlerをラムダ式で定義
    timer.Elapsed += (s, e) => Console.WriteLine(DateTime.Now);
    timer.Start();

    Console.ReadKey(); // キーが押されるまで待機

    timer.Stop();
    Console.WriteLine("タイマ停止");

    timer.Dispose();
  }
}

Imports System.Timers

Module Module1
  Sub Main()
    Dim timer As Timer = New Timer(1000) ' コンストラクタでIntervalを指定
    ' ElapsedEventHandlerをラムダ式で定義
    AddHandler timer.Elapsed, Sub(s, e) Console.WriteLine(DateTime.Now)
    timer.Start()

    Console.ReadKey() ' キーが押されるまで待機

    timer.Stop()
    Console.WriteLine("タイマ停止")

    timer.Dispose()
  End Sub
End Module

サーバベースタイマを使用したサンプルプログラム:ラムダ式バージョン(上:C#/下:VB)
この例と前の例では、明示的にDisposeメソッドを呼び出す代わりにusing句を使ってもよい。実際には、後に示すWPFの例のように、Timerクラスのインスタンス化と破棄は別の場所になることが多い(=using句が使えない)。

Windowsフォームでの利用について

 Visual Studioでプログラミングしている場合には、ツールボックスの[コンポーネント]タブから「Timerコンポーネント」をフォームにドラッグ&ドロップするだけで利用可能な状態となる。配置したTimerコンポーネントをダブルクリックすれば、タイマメソッド(Elapsedイベントハンドラ)が自動的に作成される。なお、ツールボックスに[Timer]が1つだけの場合、それはWindowsタイマ(System.Windows.Forms名前空間)なので、[ツールボックス アイテムの選択]ダイアログでSystem.Timers名前空間のTimerを追加して使用する。

 このタイマでは、スレッドタイマ(System.Threading.Timeクラス)と同様に、.NET Frameworkが管理するスレッドプールによりタイマメソッドが実行されるため、タイマメソッドはTimerクラスをインスタンス化したスレッドとは異なるスレッドで実行される。このため、これをWindowsフォームで利用する場合にはコントロールの操作に関して注意が必要となる(これについては「TIPS:Windowsフォームで別スレッドからコントロールを操作するには?を参照」)。

 しかしサーバベースタイマにはSynchronizingObjectプロパティが用意されており、このプロパティにフォームのインスタンスを設定することにより、タイマメソッドがUIスレッドで実行されるようになる。

 ただしこの場合には、もしUIスレッドで時間のかかる処理を行っていると、タイマメソッドの呼び出しが待たされることになる(SynchronizingObjectプロパティを利用した場合の挙動については「TIPS:Windowsアプリケーションでファイルやディレクトリを監視するには?」の後半部分が参考になる)。

 なお、Visual Studio .NETでTimerコンポーネントをWindowsフォームに配置した場合には、自動的にWindowsフォームのインスタンスがSynchronizingObjectプロパティに設定される。

WPFでの利用について

 WPFでは、Windowsフォームと同様にSynchronizingObjectプロパティを利用してもよいが、Dispatcherクラス(System.Windows.Threading名前空間)を使って別スレッドからUIを操作するのが一般的だ。

 Dispatcherクラスの使い方は、「タイマにより一定時間間隔で処理を行うには?(スレッドタイマ編)」で詳しく説明してある。ここでは、WPFアプリの簡単な例を紹介しよう。

 まず、画面には次のコードのようにして、TextBlockコントロールを配置しておく。

<Grid>
  <Viewbox>
    <TextBlock x:Name="TextBlock1" 
                FontFamily="Harlow Solid Italic" FontSize="36"
                TextAlignment="Center" Margin="5,0,10,0"
                Text="00:00:00" FontWeight="Bold" >
      <TextBlock.Foreground>
        <SolidColorBrush
          Color="{DynamicResource {x:Static SystemColors.HighlightColorKey}}"/>
      </TextBlock.Foreground>
    </TextBlock>
  </Viewbox>
</Grid>

WPFの画面の例(XAML)

 コードビハインドは次のコードのようになる。InitializeComponentメソッド呼び出しの後で、SetupTimerメソッドを呼び出している。そのSetupTimerメソッドでは、タイマの初期化と開始を行い、さらにウィンドウが閉じられるときにタイマを破棄するようにイベントを設定している。タイマから呼び出されるイベントハンドラはラムダ式で定義してあり、画面が持っているDispatcherインスタンスを使ってデリゲートを実行している。そのデリゲートも、ラムダ式で記述したActionデリゲートになっている。

using System;
using System.Timers;
using System.Windows;

namespace dotNetTips0374WpfCS
{
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();
      SetupTimer();
    }

    private Timer _timer;

    private void SetupTimer()
    {
      // タイマの初期化と開始
      _timer = new Timer(1000);
      // ElapsedEventHandlerをラムダ式で定義
      _timer.Elapsed += (s, e) => {
        // WPFではDispatcherを使ってUIスレッドでの処理を実行する
        this.Dispatcher.Invoke(new Action(() => {
          this.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss");
        }));
      };
      _timer.Start();

      this.Closing += (s, e) => {
        // 画面が閉じられるときに、タイマを停止して破棄
        _timer.Stop();
        _timer.Dispose();
      };
    }
  }
}

Imports System.Timers

Class MainWindow
  Public Sub New()
    InitializeComponent()
    SetupTimer()
  End Sub

  Private _timer As Timer

  Private Sub SetupTimer()
    ' タイマの初期化と開始
    _timer = New Timer(1000)
    ' ElapsedEventHandlerをラムダ式で定義
    AddHandler _timer.Elapsed,
      Sub(s, e)
        ' WPFではDispatcherを使ってUIスレッドでの処理を実行する
        Me.Dispatcher.BeginInvoke(
          New Action(
            Sub()
              Me.TextBlock1.Text = DateTime.Now.ToString("HH:mm:ss")
            End Sub
        ))
      End Sub
    _timer.Start()

    AddHandler Me.Closing,
      Sub(s, e)
        ' 画面が閉じられるときに、タイマを停止して破棄
        _timer.Stop()
        _timer.Dispose()
      End Sub

  End Sub
End Class

サーバベースタイマを使用したサンプルプログラム:WPFの例(上:C#/下:VB)
C#のnamespace宣言「dotNetTips0374WpfCS」は、適切な名前に変更してほしい。
ここでは画面が保持しているDispatcherオブジェクト(=this.Dispatcher)を使っているが、その他のコントロールのもの(例えばTextBlock1.Dispatcherなど)を使っても同じである。
なお、タイマから呼び出されるイベントハンドラをこのようにラムダ式で記述するとコンパクトにまとまるが、ラムダ式の中にラムダ式(Actionデリゲート)が入ることになって少し分かりづらいかもしれない。「タイマにより一定時間間隔で処理を行うには?(スレッドタイマ編)」のサンプルコードのようにイベントハンドラを独立したメソッドにした方が分かりやすくなるだろう。

 実行してみると次の画像のようになる。1秒ごとに時刻が書き換わる。

実行例(Windows 10) 実行例(Windows 10)
時刻文字列の色は、Windowsに設定したアクセントカラーによる。

カテゴリ:クラスライブラリ 処理対象:タイマ
カテゴリ:WPF 処理対象:スレッド
使用ライブラリ:Timerクラス(System.Timers名前空間)
使用ライブラリ:ElapsedEventHandlerデリゲート(System.Timers名前空間)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(Windowsタイマ編)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(スレッドタイマ編)
関連TIPS:Windowsフォームで別スレッドからコントロールを操作するには?
関連TIPS:Windowsアプリケーションでファイルやディレクトリを監視するには?


更新履歴

【2018/12/05】Visual Studio 2017でコードの動作検証、ラムダ式の使用例、WPFアプリでの使用例、図版の追加、全般的な構成の変更などを行いました。

【2005/11/11】初版公開。


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。