#title(.NETプログラミング研究 第22号)

#navi(.NETプログラミング研究)

#contents

*.NETプログラミング研究 第22号 [#w64698eb]

**.NET Tips [#k2d77964]

***.NETのマルチスレッドプログラミング その4 [#j46065a5]

#column(注意){{
この記事の最新版は「[[.NETのマルチスレッドプログラミング>http://dobon.net/vb/dotnet/programing/multithread.html]]」で公開しています。
}}

***ManualResetEvent、AutoResetEvent、Mutexクラスの使い方 [#b0973899]

ManualResetEvent、AutoResetEvent及びMutexクラスは、WaitHandleクラスから派生したクラスで、複数のスレッド間で同期をとるために同期オブジェクトとして使用されます。

これらのクラスのオブジェクト(「待機ハンドル」と呼ばれる)を使用して、あるスレッドの状態を別のスレッドに通知することができます。待機ハンドルの状態には「シグナル状態」と「非シグナル状態」の2つがあり、待機ハンドルをどのスレッドも所有していなければ「シグナル状態」、所有していれば「非シグナル状態」です。

スレッドが待機ハンドルの所有を要求するには、待機メソッド(WaitHandle.WaitOneなど)を呼び出します。待機メソッドは、待機ハンドルが非シグナル状態になるまで(すなわち、待機ハンドルを所有するスレッドが待機ハンドルを解放するまで)スレッドをブロックします。待機ハンドルを所有するスレッドで待機ハンドルをシグナル状態にする(すなわち、待機ハンドルを解放する)ことにより、ブロックを解除します。この仕組みを利用することにより、スレッドの同期を図ります。

(補足:ただし、これらのクラスはWin32同期ハンドルをカプセル化したものですので、Monitorクラスと比較すると移植性に劣るという欠点があります。)

''ManualResetEvent、AutoResetEventクラス''

ManualResetEvent、AutoResetEventオブジェクトは、「同期イベント」または「イベントオブジェクト」などと呼ばれています。(ここで言う「同期イベント」と、よく使われる「イベント」とを混同しないように注意してください。)

はじめに、同期イベントを使ってスレッドの同期を行う基本的な方法を紹介します。まずブロックさせたいスレッドで、非シグナル状態の同期イベントのWaitOneメソッドを呼び出し、スレッドを待機させます。スレッドをブロックする必要がなくなったところで、別のスレッドでSetメソッドを呼び出し、同期イベントをシグナル状態にして、ブロックを解除します。

次のコードでは、ManualResetEvent.WaitOneメソッドによりメインスレッドをブロックし、もう一つのスレッドがManualResetEvent.Setメソッドを呼び出すまで待機しています。

#code(vbnet){{
'Imports System.Threading
'が宣言されているものとする

Public Shared manualEvent As ManualResetEvent

'エントリポイント
Public Shared Sub Main()
    '非シグナル状態でManualResetEventオブジェクトを作成
    manualEvent = New ManualResetEvent(False)

    'スレッドを作成し、開始する
    Dim t1 As New Thread(New ThreadStart(AddressOf MyMethod))
    t1.Name = "1"
    t1.Start()

    'シグナル状態になるまでスレッドをブロックする
    manualEvent.WaitOne()

    Console.WriteLine("メインスレッド終了")

    Console.ReadLine()
End Sub

Public Shared Sub MyMethod()
    Console.WriteLine("{0}:スレッド開始", Thread.CurrentThread.Name)

    '何か仕事をするものとする
    Thread.Sleep(1000)

    'シグナル状態にする
    manualEvent.Set()

    Console.WriteLine("{0}:スレッド終了", Thread.CurrentThread.Name)
End Sub
}}

#code(csharp){{
//using System.Threading;
//が宣言されているものとする

public static ManualResetEvent manualEvent;

//エントリポイント
public static void Main()
{
    //非シグナル状態でManualResetEventオブジェクトを作成
    manualEvent = new ManualResetEvent(false);

    //スレッドを作成し、開始する
    Thread t1 = new Thread(new ThreadStart(MyMethod));
    t1.Name = "1";
    t1.Start();

    //シグナル状態になるまでスレッドをブロックする
    manualEvent.WaitOne();

    Console.WriteLine("メインスレッド終了");

    Console.ReadLine();
}

public static void MyMethod()
{
    Console.WriteLine("{0}:スレッド開始", Thread.CurrentThread.Name);

    //何か仕事をするものとする
    Thread.Sleep(1000);

    //シグナル状態にする
    manualEvent.Set();

    Console.WriteLine("{0}:スレッド終了", Thread.CurrentThread.Name);
}
}}

次にManualResetEventを使って、2つのスレッドを交互に実行する例を示します。

#code(vbnet){{
'Imports System.Threading
'が宣言されているものとする

Public Shared manualEvent1 As ManualResetEvent
Public Shared manualEvent2 As ManualResetEvent

'エントリポイント
Public Shared Sub Main()
    '非シグナル状態でManualResetEventオブジェクトを作成
    manualEvent1 = New ManualResetEvent(False)
    manualEvent2 = New ManualResetEvent(False)

    'スレッドを作成し、開始する
    Dim t1 As New Thread(New ThreadStart(AddressOf MyMethod1))
    t1.Name = "1"
    Dim t2 As New Thread(New ThreadStart(AddressOf MyMethod2))
    t2.Name = "2"
    t1.Start()
    t2.Start()

    manualEvent1.Set()

    Console.ReadLine()
End Sub

Public Shared Sub MyMethod1()
    Dim i As Integer
    For i = 0 To 99
        'manualEvent1がシグナル状態になるまでスレッドをブロック
        manualEvent1.WaitOne()
        'manualEvent1を非シグナル状態にする
        manualEvent1.Reset()

        Console.Write(Thread.CurrentThread.Name)

        'manualEvent2をシグナル状態にする
        manualEvent2.Set()
    Next i
End Sub

Public Shared Sub MyMethod2()
    Dim i As Integer
    For i = 0 To 99
        'manualEvent2がシグナル状態になるまでスレッドをブロック
        manualEvent2.WaitOne()
        'manualEvent2を非シグナル状態にする
        manualEvent2.Reset()

        Console.Write(Thread.CurrentThread.Name)

        'manualEvent1をシグナル状態にする
        manualEvent1.Set()
    Next i
End Sub
}}

#code(csharp){{
//using System.Threading;
//が宣言されているものとする

public static ManualResetEvent manualEvent1;
public static ManualResetEvent manualEvent2;

//エントリポイント
public static void Main()
{
    //非シグナル状態でManualResetEventオブジェクトを作成
    manualEvent1 = new ManualResetEvent(false);
    manualEvent2 = new ManualResetEvent(false);

    //スレッドを作成し、開始する
    Thread t1 = new Thread(new ThreadStart(MyMethod1));
    t1.Name = "1";
    Thread t2 = new Thread(new ThreadStart(MyMethod2));
    t2.Name = "2";
    t1.Start();
    t2.Start();

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

    Console.ReadLine();
}

public static void MyMethod1()
{
    for (int i = 0; i < 100; i++)
    {
        //manualEvent1がシグナル状態になるまでスレッドをブロック
        manualEvent1.WaitOne();
        //manualEvent1を非シグナル状態にする
        manualEvent1.Reset();

        Console.Write(Thread.CurrentThread.Name);

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

public static void MyMethod2()
{
    for (int i = 0; i < 100; i++)
    {
        //manualEvent2がシグナル状態になるまでスレッドをブロック
        manualEvent2.WaitOne();
        //manualEvent2を非シグナル状態にする
        manualEvent2.Reset();

        Console.Write(Thread.CurrentThread.Name);

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

#pre{{
//出力結果例:
//1212121212121212121212121212121212121212121212121212121212121212121212
//1212121212121212121212121212121212121212121212121212121212121212121212
//121212121212121212121212121212121212121212121212121212121212
}}

上記のコードではManualResetEvent.WaitOneメソッドの後すぐにResetメソッドを呼び出して非シグナル状態に戻していますが、ManualResetEventの代わりにAutoResetEventを使うことにより、この手間が省けます。AutoResetEventでは、シグナルを待機中のスレッドがすべて解放されると、自動的に非シグナル状態にリセットされるのです。(待機中のスレッドがない場合は、無限にシグナル状態のままとなります。)

以下に上記のコードをAutoResetEventを使って書き換えた例を示します。

#code(vbnet){{
'Imports System.Threading
'が宣言されているものとする

Public Shared autoEvent1 As AutoResetEvent
Public Shared autoEvent2 As AutoResetEvent

'エントリポイント
Public Shared Sub Main()
    '非シグナル状態でAutoResetEventオブジェクトを作成
    autoEvent1 = New AutoResetEvent(False)
    autoEvent2 = New AutoResetEvent(False)

    'スレッドを作成し、開始する
    Dim t1 As New Thread(New ThreadStart(AddressOf MyMethod1))
    t1.Name = "1"
    Dim t2 As New Thread(New ThreadStart(AddressOf MyMethod2))
    t2.Name = "2"
    t1.Start()
    t2.Start()

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

    Console.ReadLine()
End Sub

Public Shared Sub MyMethod1()
    Dim i As Integer
    For i = 0 To 99
        'autoEvent1がシグナル状態になるまでスレッドをブロック
        autoEvent1.WaitOne()

        Console.Write(Thread.CurrentThread.Name)

        'autoEvent2をシグナル状態にする
        autoEvent2.Set()
    Next i
End Sub

Public Shared Sub MyMethod2()
    Dim i As Integer
    For i = 0 To 99
        'autoEvent2がシグナル状態になるまでスレッドをブロック
        autoEvent2.WaitOne()

        Console.Write(Thread.CurrentThread.Name)

        'autoEvent1をシグナル状態にする
        autoEvent1.Set()
    Next i
End Sub
}}

#code(csharp){{
//using System.Threading;
//が宣言されているものとする

public static AutoResetEvent autoEvent1;
public static AutoResetEvent autoEvent2;

//エントリポイント
public static void Main()
{
    //非シグナル状態でAutoResetEventオブジェクトを作成
    autoEvent1 = new AutoResetEvent(false);
    autoEvent2 = new AutoResetEvent(false);

    //スレッドを作成し、開始する
    Thread t1 = new Thread(new ThreadStart(MyMethod1));
    t1.Name = "1";
    Thread t2 = new Thread(new ThreadStart(MyMethod2));
    t2.Name = "2";
    t1.Start();
    t2.Start();

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

    Console.ReadLine();
}

public static void MyMethod1()
{
    for (int i = 0; i < 100; i++)
    {
        //autoEvent1がシグナル状態になるまでスレッドをブロック
        autoEvent1.WaitOne();

        Console.Write(Thread.CurrentThread.Name);

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

public static void MyMethod2()
{
    for (int i = 0; i < 100; i++)
    {
        //autoEvent2がシグナル状態になるまでスレッドをブロック
        autoEvent2.WaitOne();

        Console.Write(Thread.CurrentThread.Name);

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

複数の待機ハンドルのシグナル状態により待機が解除されるようにするには、WaitOneではなく、WaitAllまたはWaitAnyメソッドを使用します。WaitAllメソッドでは指定されたすべての待機ハンドルがシグナル状態になるまで待機し、WaitAnyメソッドでは指定されたいずれかの待機ハンドルがシグナル状態になるまで待機します。

''Mutexクラス''

Mutexは同時に1つのスレッドでしか所有できない同期オブジェクトです。

Mutexを使用して同期を行う基本的な方法は、次のようなものです。まずミューテックスの所有権を持たないスレッドがWaitOneメソッドを呼び出して所有権を要求します。この時ミューテックスの所有権を持つスレッドがなければ、このスレッドが所有権を取得しますが、別のスレッドが所有権を取得していれば、WaitOneメソッドを呼び出したスレッドは待機状態になります。ミューテックスを所有しているスレッドがReleaseMutexメソッドを呼び出すか、あるいは正常終了してミューテックスの所有を解放すると、待機中の次のスレッドが所有権を取得します。

なお、ミューテックスを所有しているスレッドがさらにWaitOneメソッドを繰り返した場合、ミューテックスの所有を解放するためには、ReleaseMutexメソッドを同じだけ呼び出す必要があります。

Mutexを使用した簡単な例を次に示します。MyMethod1メソッドのMutex.WaitOneとMutex.ReleaseMutexで囲まれた所が1つのスレッドのみが入れる同期された部分となります。

#code(vbnet){{
'Imports System.Threading
'が宣言されているものとする

Public Shared mut As Mutex

'エントリポイント
Public Shared Sub Main()
    'Mutexオブジェクトを作成(初期所有権なし)
    mut = New Mutex

    '2つのスレッドを作成し、開始する
    Dim t1 As New Thread(New ThreadStart(AddressOf MyMethod1))
    t1.Name = "1"
    Dim t2 As New Thread(New ThreadStart(AddressOf MyMethod1))
    t2.Name = "2"
    t1.Start()
    t2.Start()

    Console.ReadLine()
End Sub

Public Shared Sub MyMethod1()
    Dim i As Integer
    For i = 0 To 99
        'ミューテックスの所有権を要求する
        '取得できない時は、取得できるまで待機
        mut.WaitOne()

        '同期されたコードブロック
        Console.Write(Thread.CurrentThread.Name)

        'Mutexを解放する
        mut.ReleaseMutex()
    Next i
End Sub
}}

#code(csharp){{
//using System.Threading;
//が宣言されているものとする

public static Mutex mut;

//エントリポイント
public static void Main()
{
    //Mutexオブジェクトを作成(初期所有権なし)
    mut = new Mutex();

    //2つのスレッドを作成し、開始する
    Thread t1 = new Thread(new ThreadStart(MyMethod1));
    t1.Name = "1";
    Thread t2 = new Thread(new ThreadStart(MyMethod1));
    t2.Name = "2";
    t1.Start();
    t2.Start();

    Console.ReadLine();
}

public static void MyMethod1()
{
    for (int i = 0; i < 100; i++)
    {
        //ミューテックスの所有権を要求する
        //取得できない時は、取得できるまで待機
        mut.WaitOne();

        //同期されたコードブロック
        Console.Write(Thread.CurrentThread.Name);

        //Mutexを解放する
        mut.ReleaseMutex();
    }
}
}}

Mutexの大きな特徴としてはさらに、スレッド間の同期だけではなく、プロセス間の同期にも使用できる点があげられます。アプリケーションの二重起動を防止するためにMutexを使用する方法は、「.NET Tips - 二重起動を禁止する」で紹介しています。

-[[.NET Tips - 二重起動を禁止する>http://dobon.net/vb/dotnet/process/checkprevinstance.html]]

参考:

-[[WaitHandle>http://www.microsoft.com/japan/msdn/library/ja/cpguide/html/cpconwaithandle.asp]]
-[[WaitHandle クラス>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemthreadingwaithandleclasstopic.asp]]
-[[AutoResetEvent クラス>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemthreadingautoreseteventclasstopic.asp]]
-[[ManualResetEvent クラス>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemthreadingmanualreseteventclasstopic.asp]]
-[[Mutex クラス>http://www.microsoft.com/japan/msdn/library/ja/cpref/html/frlrfsystemthreadingmutexclasstopic.asp]]
-[[高度な同期化技法>http://www.microsoft.com/japan/msdn/library/ja/vbcn7/html/vatchAdvancedSynchronizationTechniques.asp]]

**コメント [#s274d65c]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]],2003-12-03 (水) 06:00:00,DOBON!,2010-03-21 (日) 01:09:35,DOBON!)

[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]