PG日誌

受託系 PG が C# の事を書いています

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) =>
            {
                try
                {
                    timer.Stop(); // もしくは timer.Enabled = false;

                    // 何らかの処理
                    Console.WriteLine("Ticks = " + DateTime.Now.Ticks);
                }
                finally
                {
                    timer.Start(); // もしくは timer.Enabled = true;
                }
            };

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

            Console.ReadLine();

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

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

System.Threading.Timerの使い方

こちらは、一回作成すると設定したタイマーイベントの内容は変更できません。また、生成後にコンストラクタの"dueTime"ミリ秒後から"period"ミリ秒間隔で処理が開始されます。

停止する時も以下の通り、発生間隔を無限にすることで停止します。制作者なりの親切のつもりでしょうが独特の感覚だと思います。

この例の場合、タイマーインスタンス作成後、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){ }
        }
    }
}

まとめ

.oO(Therading.Timerだけ使い方使い方が違う!)

学習コストを考えてTimers.Timerを常に選んだ方が脳に優しいと思います。

また、当方説明ではタイマーイベントが発生した時に冒頭でタイマーを停止して処理完了後に再開していますが、ハンドラ内の時間がかかって次のイベントが発生して次のイベントが発生してほしくない人は必須の方法だと思います。間違ってもlock構文でロックしないようにしてください。