SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

.NET Frameworkにおけるマルチスレッドプログラミングの基本

.NETマルチスレッドプログラミング 3:プロセス間同期とスレッドタイマ

.NET Frameworkにおけるマルチスレッドプログラミングの基本


  • X ポスト
  • このエントリーをはてなブックマークに追加

スレッドタイマ

 .NET Frameworkで用意されているタイマには、次の3種類があります。

  • System.Windows.Forms.Timer(Windowsタイマ)
  • System.Threading.Timer(スレッドタイマ)
  • System.Timers.Timer(サーバータイマ)

 最も馴染み深いタイマは、Windowsタイマでしょう。Windowsアプリケーションプロジェクトのフォームデザイナで[ツールボックス]の[Windowsフォーム]内に[Timer]としてあるのが、Windowsタイマです。最もよく使われるタイマですが、OSのタイマ機能を使用しているため、スレッドでメッセージを処理しなければタイマイベントは発生せず、長い処理によりブロックされる恐れがあります。これに対してサーバータイマとスレッドタイマはワーカースレッドにより処理されますので、その心配がありません。

 フォームデザイナで[ツールボックス]の[コンポーネント]内にある[Timer]がサーバータイマです。パート2のスレッドプールの解説で触れたとおり、サーバータイマはスレッドプールを使用します。なお、サーバータイマについてMSDNライブラリの「サーバー ベースのタイマの概説」には、「サーバーベースのタイマは、サーバー環境での実行用に最適化された従来のタイマを強化したものです」とあります。

 スレッドタイマもスレッドプールを使用したタイマです。イベントの代わりにコールバックメソッドを使用します。軽量で、簡単なタイマ処理が必要な時に便利です。

 サンプル「timer_01」でスレッドタイマの使用例を示します。

「VB.NET/timer_01/Class1.vb」 (VB.NET)
Class Class1
    'エントリポイント
    Public Shared Sub Main()
        'Threading.Timerオブジェクトの作成
        'TimerState.Tickを0.1秒後に1秒間隔で実行する
        Dim tmr As New System.Threading.Timer( _
            New System.Threading.TimerCallback(AddressOf Tick), _
            Nothing, 100, 1000)

        System.Threading.Thread.Sleep(5300)

        'タイマの開始時間とコールバックメソッドの呼び出し間隔を変更
        tmr.Change(500, 500)
        Console.WriteLine("Changed")

        System.Threading.Thread.Sleep(5300)

        'コールバックメソッドの呼び出しを停止
        tmr.Change(System.Threading.Timeout.Infinite, _
            System.Threading.Timeout.Infinite)
        Console.WriteLine("Stop")

        System.Threading.Thread.Sleep(5300)

        'タイマを破棄
        tmr.Dispose()
        Console.WriteLine("Disposed")

        '待機する
        Console.ReadLine()
    End Sub

    Public Shared Sub Tick(ByVal state As Object)
        '表示
        Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _
            System.Environment.TickCount)
    End Sub
End Class
「C#/timer_01/Class1.cs」 (C#)
class Class1
{
    //エントリポイント
    public static void Main()
    {
        //Threading.Timerオブジェクトの作成
        //TimerState.Tickを0.1秒後に1秒間隔で実行する
        System.Threading.Timer tmr = 
            new System.Threading.Timer(
            new System.Threading.TimerCallback(Tick),
            null, 100, 1000);

        System.Threading.Thread.Sleep(5300);

        //タイマの開始時間とコールバックメソッドの呼び出し間隔を変更
        tmr.Change(500, 500);
        Console.WriteLine("Changed");

        System.Threading.Thread.Sleep(5300);

        //コールバックメソッドの呼び出しを停止
        tmr.Change(System.Threading.Timeout.Infinite,
            System.Threading.Timeout.Infinite);
        Console.WriteLine("Stop");

        System.Threading.Thread.Sleep(5300);

        //タイマを破棄
        tmr.Dispose();
        Console.WriteLine("Disposed");

        //待機する
        Console.ReadLine();
    }

    public static void Tick(object state)
    {
        //表示
        Console.WriteLine("システム起動後の経過時間:{0}ミリ秒",
            System.Environment.TickCount);
    }
}

 スレッドタイマを作成するには、インスタンス作成時のコンストラクタで実行するメソッドへのTimerCallbackデリゲート、メソッドで使用される状態オブジェクト、最初に発生する時間、発生する時間間隔を渡します。最初に発生する時間、発生する時間間隔を変更するには、Changeメソッドを使います。タイマを破棄するには、Disposeメソッドを呼び出します。タイマの破棄は必要です。

 タイマを破棄するのではなく、一時的に停止させたい場合は、Changeメソッドで開始時間をTimeout.Infiniteにするという方法があります。また、コールバックメソッドを一回だけ実行したいときは、呼び出し間隔をTimeout.Infiniteにするという方法があります。

 サンプル「timer_01」では、Tickメソッドをスレッドタイマにより定期的に実行します。まず0.1秒後に1秒間隔でTickメソッドが呼び出されるようにしています。その後5秒後に、Changeメソッドで開始時間と間隔を0.5秒に変更します。さらにその5秒後に開始時間をTimeout.Infiniteにすることによりタイマを一時停止し、その5秒後にタイマを破棄しています。

 スレッドタイマを使用する手順をまとめると、次のようになります。

  1. タイマで定期的に呼び出すメソッドとして、TimerCallbackデリゲートと同じシグネチャのメソッドを作成する。
  2. TimerCallbackデリゲートオブジェクト、状態オブジェクト、開始時間、時間間隔を指定して、Timerオブジェクトを作成する。
  3. 開始時間と間隔を変更するときは、Changeメソッドを呼び出す。
  4. タイマが不要になったときにDisposeメソッドを呼び出す。

ThreadPool.RegisterWaitForSingleObjectメソッド

 ThreadPool.QueueUserWorkItemメソッドよりも、より多くの機能を持つメソッドがThreadPool.RegisterWaitForSingleObjectメソッドです。QueueUserWorkItemメソッドと比較してRegisterWaitForSingleObjectメソッドの特徴は、WaitHandleを使って実行を待機できる点と、タイムアウトを指定できる点にあります。

WaitHandleによる待機

 WaitHandleオブジェクトを使用した例が、サンプル「threadpool2_01」です。

「VB.NET/threadpool2_01/Class1.vb」 (VB.NET)
Class Class1
    'エントリポイント
    Public Shared Sub Main()
        '非シグナル状態でManualResetEventを作成
        Dim ev As New System.Threading.ManualResetEvent(False)

        Dim i As Integer
        For i = 0 To 9
            'デリゲートをキューに追加する
            System.Threading.ThreadPool.RegisterWaitForSingleObject( _
                ev, _
                New System.Threading.WaitOrTimerCallback( _
                    AddressOf WriteTime), _
                Nothing, _
                -1, _
                True)
        Next i

        '待機する
        Console.WriteLine("Enterキーで開始")
        Console.ReadLine()

        'ManualResetEventをシグナル状態にする
        ev.Set()

        Console.ReadLine()
    End Sub

    'システム起動後の経過時間を表示
    Private Shared Sub WriteTime(ByVal state As Object, _
        ByVal timedOut As Boolean)
        Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _
            System.Environment.TickCount)
    End Sub
End Class
「C#/threadpool2_01/Class1.cs」 (C#)
class Class1
{
    //エントリポイント
    public static void Main()
    {
        //非シグナル状態でManualResetEventを作成
        System.Threading.ManualResetEvent ev =
            new System.Threading.ManualResetEvent(false);

        for (int i = 0; i < 10; i++)
        {
            //デリゲートをキューに追加する
            System.Threading.ThreadPool.RegisterWaitForSingleObject( 
                ev,
                new System.Threading.WaitOrTimerCallback(WriteTime),
                null,
                -1,
                true
                );
        }

        //待機する
        Console.WriteLine("Enterキーで開始");
        Console.ReadLine();

        //ManualResetEventをシグナル状態にする
        ev.Set();

        Console.ReadLine();
    }

    //システム起動後の経過時間を表示
    private static void WriteTime(object state, bool timedOut)
    {
        Console.WriteLine("システム起動後の経過時間:{0}ミリ秒",
            System.Environment.TickCount);
    }
}

 RegisterWaitForSingleObjectメソッドによりWriteTimeメソッドが10回スレッドプールのキューに追加されますが、WaitHandleオブジェクトのevが非シグナル状態のため、すぐには実行されません。Setメソッドによりevがシグナル状態になると待機が解除され、ワーカースレッドにより一斉に実行されるようになります。

タイムアウト

 サンプル「threadpool2_02」では、さらにタイムアウトを使用した例を紹介しています。

「VB.NET/threadpool2_02/Class1.vb」 (VB.NET)
Class Class1
    'エントリポイント
    Public Shared Sub Main()
        '非シグナル状態でAutoResetEventを作成
        Dim ev As New System.Threading.AutoResetEvent(False)

        'デリゲートをキューに追加する
        '1秒おきに実行する
        System.Threading.ThreadPool.RegisterWaitForSingleObject( _
            ev, _
            New System.Threading.WaitOrTimerCallback( _
                AddressOf WriteTime), _
            Nothing, _
            1000, _
            False)

        Dim i As Integer
        For i = 0 To 9
            '3秒待機する
            System.Threading.Thread.Sleep(3000)
            'AutoResetEventをシグナル状態にする
            ev.Set()
        Next i

        Console.ReadLine()
    End Sub

    'システム起動後の経過時間を表示
    Private Shared Sub WriteTime(ByVal state As Object, _
        ByVal timedOut As Boolean)
        Console.WriteLine( _
            "システム起動後の経過時間:{0}ミリ秒 / タイムアウト:{1}", _
            System.Environment.TickCount, timedOut)
    End Sub
End Class
「C#/threadpool2_02/Class1.cs」 (C#)
class Class1
{
    //エントリポイント
    public static void Main()
    {
        //非シグナル状態でAutoResetEventを作成
        System.Threading.AutoResetEvent ev =
            new System.Threading.AutoResetEvent(false);

        //デリゲートをキューに追加する
        //1秒おきに実行する
        System.Threading.ThreadPool.RegisterWaitForSingleObject( 
            ev,
            new System.Threading.WaitOrTimerCallback(WriteTime),
            null,
            1000,
            false
            );

        for (int i = 0; i < 10; i++)
        {
            //3秒待機する
            System.Threading.Thread.Sleep(3000);
            //AutoResetEventをシグナル状態にする
            ev.Set();
        }

        Console.ReadLine();
    }

    //システム起動後の経過時間を表示
    private static void WriteTime(object state, bool timedOut)
    {
        Console.WriteLine(
            "システム起動後の経過時間:{0}ミリ秒 / タイムアウト:{1}",
            System.Environment.TickCount, timedOut);
    }
}

 このプログラムを実行すると、例えば次のように出力されます。ほぼ1秒ごとに「タイムアウト」が「True」となっている出力と、3秒ごとに「タイムアウト」が「False」となっている出力が続いていることが分かります。

システム起動後の経過時間:13193601ミリ秒 / タイムアウト:True
システム起動後の経過時間:13194592ミリ秒 / タイムアウト:True
システム起動後の経過時間:13195594ミリ秒 / タイムアウト:False
システム起動後の経過時間:13196595ミリ秒 / タイムアウト:True
システム起動後の経過時間:13197597ミリ秒 / タイムアウト:True
システム起動後の経過時間:13198598ミリ秒 / タイムアウト:True
システム起動後の経過時間:13198598ミリ秒 / タイムアウト:False
システム起動後の経過時間:13199600ミリ秒 / タイムアウト:True
システム起動後の経過時間:13200591ミリ秒 / タイムアウト:True
システム起動後の経過時間:13201592ミリ秒 / タイムアウト:True
システム起動後の経過時間:13201592ミリ秒 / タイムアウト:False
システム起動後の経過時間:13202594ミリ秒 / タイムアウト:True
(以下省略)

 ここでは、タイムアウトする時間を4番目の引数に指定してRegisterWaitForSingleObjectメソッドを呼び出しています。このときは、WaitHandleがシグナル状態にならなくても、タイムアウト間隔が経過するとWriteTimeメソッドが実行されます。

 また、RegisterWaitForSingleObjectメソッドの5番目の引数をfalseにして、WriteTimeメソッドが何回も実行されるようにしています。このようにタイムアウトと併せて使用することにより、タイムアウトの時間が経過するたびに何回もデリゲートが実行され、タイマのような働きをします。

 WriteTimeメソッドがシグナル通知により実行されたか、タイムアウトにより実行されたかは、WriteTimeメソッドのtimedOut引数から分かります。タイムアウトにより実行された時はtrueとなります。

Unregisterメソッド

 WaitHandleがシグナル状態になるまで、またはタイムアウトするまで実行を待機するという待機操作は、RegisteredWaitHandle.Unregisterメソッドによりキャンセルすることができます。その例をサンプル「threadpool2_03」で示します。

「VB.NET/threadpool2_03/Class1.vb」 (VB.NET)
'状態オブジェクトのためのクラス
Class TaskInfo
    '待機操作のためのRegisteredWaitHandle
    Public Handle As System.Threading.RegisteredWaitHandle = Nothing

    'メソッドに渡すデータ
    Public StringValue As String

    Public Sub New(ByVal str As String)
        StringValue = str
    End Sub
End Class

Class Class1
    'エントリポイント
    Public Shared Sub Main()
        'TaskInfoオブジェクトの作成
        Dim ti As New TaskInfo("こんにちは")

        '非シグナル状態でAutoResetEventを作成
        Dim ev As New System.Threading.AutoResetEvent(False)

        'デリゲートをキューに追加する
        '状態オブジェクトを指定して、1秒おきに実行する
        ti.Handle = _
            System.Threading.ThreadPool.RegisterWaitForSingleObject( _
                ev, _
                New System.Threading.WaitOrTimerCallback( _
                    AddressOf WriteTime), _
                ti, _
                1000, _
                False)

        '待機
        Console.ReadLine()

        'AutoResetEventをシグナル状態にする
        ev.Set()

        Console.ReadLine()
    End Sub

    'システム起動後の経過時間を表示
    Private Shared Sub WriteTime(ByVal state As Object, _
        ByVal timedOut As Boolean)
        '状態オブジェクトの取得
        Dim ti As TaskInfo = CType(state, TaskInfo)

        '表示
        Console.WriteLine("システム起動後の経過時間:{0}ミリ秒", _
            System.Environment.TickCount)

        'シグナルが送信されたら
        'コールバックメソッドが実行されないように
        '待機操作をキャンセルする
        If Not timedOut AndAlso Not (ti.Handle Is Nothing) Then
            ti.Handle.Unregister(Nothing)
        End If
    End Sub
End Class
「C#/threadpool2_03/Class1.cs」 (C#)
//状態オブジェクトのためのクラス
class TaskInfo
{
    //待機操作のためのRegisteredWaitHandle
    public System.Threading.RegisteredWaitHandle Handle = null;

    //メソッドに渡すデータ
    public string StringValue;

    public TaskInfo(string str)
    {
        StringValue = str;
    }
}

class Class1
{
    //エントリポイント
    public static void Main()
    {
        //TaskInfoオブジェクトの作成
        TaskInfo ti = new TaskInfo("こんにちは");

        //非シグナル状態でAutoResetEventを作成
        System.Threading.AutoResetEvent ev =
            new System.Threading.AutoResetEvent(false);

        //デリゲートをキューに追加する
        //状態オブジェクトを指定して、1秒おきに実行する
        ti.Handle =
            System.Threading.ThreadPool.RegisterWaitForSingleObject( 
            ev,
            new System.Threading.WaitOrTimerCallback(WriteTime),
            ti,
            1000,
            false
            );

        //待機
        Console.ReadLine();

        //AutoResetEventをシグナル状態にする
        ev.Set();

        Console.ReadLine();
    }

    //システム起動後の経過時間を表示
    private static void WriteTime(object state, bool timedOut)
    {
        //状態オブジェクトの取得
        TaskInfo ti = (TaskInfo) state;

        //表示
        Console.WriteLine("システム起動後の経過時間:{0}ミリ秒",
            System.Environment.TickCount);

        //シグナルが送信されたら
        //コールバックメソッドが実行されないように
        //待機操作をキャンセルする
        if (!timedOut && ti.Handle != null)
            ti.Handle.Unregister(null);
    }
}

 この例では1秒おきにWriteTimeメソッドが実行されますが、AutoResetEvent.Setメソッドによりシグナルを送ってWriteTimeメソッドを実行すると、「Unregister(null)」により、それ以上は実行されなくなります。

まとめ

 パート3では、Mutexによるプロセス間の同期や、スレッドタイマの利用方法などについて学びました。次回(パート4)は、スレッドの優先順位を変更したり、スレッドを強制終了・一時停止したりする方法について解説します。

参考資料

  1. Jon's Homepage! 『Multi-threading in .NET』
  2. MSDNライブラリ 『.NET Framework開発者ガイド-スレッド処理』
  3. MSDNライブラリ 『Visual Basic言語の概念-Visual Basic .NETにおけるマルチスレッド』
  4. MSDNライブラリ 『.NET Framework開発者ガイド-非同期呼び出しの組み込み』
  5. MSDNライブラリ 『コンポーネントのマルチスレッド』
  6. MSDNライブラリ 『スレッド処理のデザイン ガイドライン』
  7. MSDNライブラリ 『非同期プログラミングのガイドライン』
  8. MSDNライブラリ 『Chapter 4 - Architecture and Design Review of a .NET Application for Performance and Scalability』
修正履歴

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
.NET Frameworkにおけるマルチスレッドプログラミングの基本連載記事一覧

もっと読む

この記事の著者

どぼん!(ドボン!)

DOBON.NET内で.NET Frameworkの機能を紹介したWebサイト.NET Tipsやメールマガジン「.NETプログラミング研究」の発行人。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/140 2007/06/09 02:34

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング