C#の4つのTimerの用途と使い方

C#というか.NETのタイマーの種類について整理と説明をしたいと思います。

.NETには主に 4種類のタイマーがあります。

種類 アセンブリ 用途
System.Timers.Timer System 普通の定周期処理
System.Threading.Timer mscorlib 普通の定周期処理
System.Windows.Forms.Timer System.Windows.Forms WinForm GUI専用
System.Windows.Threading.DispatcherTimer WindowsBase WPF GUI専用

Timers.TimerとThreading.Timerの概要

この2つのタイマーの精度はほぼ同等です。但し、16ms以下のインターバルはOSの制約から指定しても意味がありません。 詳細はこちらの記事を参照ください。

何が違うのかというと、System.Timers.Timerの方は以下のプロパティが実装されています。

public ISynchronizeInvoke SynchronizingObject { get; set; }

このプロパティにオブジェクトを設定すると、そのオブジェクトが生成されたスレッドと同じスレッドでElapsedが発生するようになります。WindowsFromでFormのメインスレッドでオブジェクト作成しSynchronizingObjectに設定するとElapsedはメインスレッドで発生するようになります。既定値はnullでnullの場合、別のスレッドで処理が行われます。詳細はこちらを参照ください

え、精度落ちるんじゃないの・・・?と、思ったのですがそんな用途に使わないので無視しても大丈夫だと思います。

また、どちらを使った方がよりベターか?という件ですが、この2つはどちらを使っても大差ないので、[使いやすい|好きな方]を使って大丈夫です。

Forms.TimerとDispatcherTimerの概要

それぞれ.NET Frameworkが持つ2つのGUIシステム向けに作成されています。それぞれのメッセージループの仕組みを意識して作られているので各々のGUIテクノロジ専用のタイマーとなります。

この2つのタイマーはイベントが発生するとそのイベントはGUIスレッド上で発生します。従って、この2つのTimerに設定したイベントハンドラ内でGUIコントロールを操作した場合は例外(InvaliedOperationException)が発生しません。

この2つのタイマーは前述のタイマーより精度が低いです。指定した時間に発生することを期待してはいけません。またUIで別の処理が進んでいる場合、ハンドラ内の処理が待たされる時もあります。

4種類のオブジェクトの使い方

それぞれ使用方法が異なるので、それぞれのタイマーの使い方を紹介したいと思います。(とは言っても、Threading.Timerが異色を放っている以外は大体同じです。

System.Timers.Timerの使い方

このタイマーは以下例の通りイベントを付け替えたり、停止したり再開がメソッドやプロパティで行うことができます。(余談ですがmこのタイマーMarshalByRefObjectを継承しているので、Serializeできるってことですかね?試したこと無いですが…)

using System;
using System.Timers;

namespace TimerSample
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            // 開始時に間隔を指定する
            var timer = new Timer(100/*msec*/);

            // Elapsedイベントにタイマー発生時の処理を設定する
            timer.Elapsed += (sender, e) =>
            {
                // 何らかの処理
                Console.WriteLine("Ticks = " + DateTime.Now.Ticks);
            };

            // タイマーを開始する
            timer.Start();

            Console.ReadLine();

            // タイマーを停止する
            timer.Stop();

            // 資源の解放
            using(timer){ }
        }
    }
}

System.Threading.Timerの使い方

System.Threading.Timer はいちど作成すると設定した処理内容は変更できません。生成後にコンストラクタの dueTime ミリ秒後から period ミリ秒間隔で処理が実行されます。

停止する時は、Change メソッドを使用します。発生間隔を無限にすることで停止します。指定方法は少し独特です。

この例の場合、タイマーインスタンス作成後、100ミリ秒後から50ミリ秒間隔で callback に指定したラムダが呼び出されます。

using System;
using System.Threading;

namespace TimerSample
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            TimerCallback callback = state =>
            {
                Console.WriteLine("Ticks = " + DateTime.Now.Ticks);
            };

            var timer = new Timer(callback, null, 100/* msec */, 50 /* msec*/);

            Console.ReadLine();

            // 無限に設定することで停止を表す
            timer.Change(Timeout.Infinite, Timeout.Infinite);

            // 資源の解放
            using (timer) { }
        }
    }
}

System.Windows.Forms.Timerの使い方

WinForm向けのタイマーです。(久しぶりにWinFrom触りました)Tickに設定するハンドラのシグネチャが以下のようにまんま、フォームのイベントハンドラなのでいかにもForm専用な雰囲気です。

// タイマーが発生したときの処理を設定するイベント
public event EventHandler Tick;

使い方ですが、 System.Timers.Timerとほぼ同じです。停止・開始をメソッドで、ハンドラをイベントに設定する形になります。

また、以下のように自分で書かなくてもイベントハンドラさえ書いておけば、GUIデザイナのツールボックスからD&Dで作成できます。

using System;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class MyForm : Form
    {
        private Timer timer;

        public MyForm()
        {
            this.InitializeComponent();

            // 自分で作る場合フォームのコンポーネントを設定しておく
            this.timer = new Timer(this.components);
            this.timer.Start();

            // Tickイベントにタイマーが発生した時の処理を書く
            this.timer.Tick += (semder, e) =>
            {
                this.label.Text = "Tick = " + DateTime.Now.Ticks;
            };

            // フォームが閉じるときに以下を記述(まぁやらないくても・・・って感じですが

            // タイマーを停止
            this.timer.Stop();

            // 資源を解放
            using(this.timer){ }
        }
    }
}

System.Windows.Threading.DispatcherTimerの使い方

このタイマーも難しいことは無いのですが、WPFのMainWindowでコードを書いていきます。こちらも使い方はSystem.Timers.Timerとほぼ同じです。

先に以下のような画面を用意します。

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBlock x:Name="TextBlock"/>
    </Grid>
</Window>

コードビハインドは以下の通り。

using System;
using System.Windows;
using System.Windows.Threading;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private DispatcherTimer timer;

        public MainWindow()
        {
            this.InitializeComponent();

            // インターバルがTimeSpan型なので注意
            this.timer = new DispatcherTimer
            {
                Interval = TimeSpan.FromMilliseconds(50)
            };

            // 発生時の処理を記述
            this.timer.Tick += (sender, e) =>
            {
                // 直接触っても例外が起きない
                this.TextBlock.Text = "Tick = " + DateTime.Now.Ticks;
            };

            this.timer.Start();

            // 以下終了するときに記述する

            this.timer.Stop();
            using(this.timer){ }
        }
    }
}

まとめ

System.Therading.Timer だけ使い方使い勝手が違います。

UI処理では適切なタイマーを選択し、UI以外のバックグラウンド処理では柔軟性が高い Timers.Timer を常に選択すればよさそうです。