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

.NET TIPS:タイマにより一定時間間隔で処理を行うには?(スレッドタイマ編)

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

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

連載「.NET TIPS」

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


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

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

 本稿では2のスレッドタイマについて、その基本的な使い方をまとめる。

POINT スレッドタイマの使い方

スレッドタイマの使い方まとめ スレッドタイマの使い方まとめ


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

スレッドタイマ: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

スレッドタイマを使用したサンプルプログラム(上:C#/下:VB)
C#版サンプルプログラムのダウンロード
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

スレッドタイマを使用したサンプルプログラム:ラムダ式バージョン(上:C#/下:VB)
この例と前の例では、明示的に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>

WPFの画面の例(XAML)
省略した部分では、コントロールの位置やサイズ、フォントや色などを指定している。

 コードビハインドは次のコードのようになる。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

スレッドタイマを使用したサンプルプログラム:WPFの例(上:C#/下:VB)
C#のnamespace宣言「dotNetTips0372WpfCS」は、適切な名前に変更してほしい。
ここでは画面が保持しているDispatcherオブジェクト(=this.Dispatcher)を使っているが、その他のコントロールのもの(例えばthis.textBlock.Dispatcherなど)を使っても同じである。
なお、MyTimerCallbackメソッドはTimerCallbackデリゲートとして使うだけなので、タイマを生成する場所にラムダ式として記述してもよい。ただそうすると、ラムダ式の中にラムダ式(Actionデリゲート)が入ることになって少し分かりづらくなるので、あえてMyTimerCallbackメソッドとして独立させた。

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

実行例(Windows 10) 実行例(Windows 10)

カテゴリ:クラスライブラリ 処理対象:スレッド
カテゴリ:クラスライブラリ 処理対象:タイマ
使用ライブラリ:Timerクラス(System.Threading名前空間)
使用ライブラリ:TimerCallbackデリゲート(System.Threading名前空間)
使用ライブラリ:Timeoutクラス(System.Threading名前空間)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(Windowsタイマ編)
関連TIPS:タイマにより一定時間間隔で処理を行うには?(サーバベース・タイマ編)
関連TIPS:Windowsフォームで別スレッドからコントロールを操作するには?


更新履歴

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

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


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

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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