Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

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

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

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/08/10 12:00

本稿は、.NET Frameworkにおけるマルチスレッドプログラミングの解説です。四部作の「パート3」では、Mutexによるプロセス間の同期や、スレッドタイマの利用方法などについて学びます。

目次

目次

はじめに

 パート1パート2では、筆者が.NET Frameworkにおけるマルチスレッドプログラミングで、必ず知っておくべきと判断した事柄を紹介しました。パート3では、その他に知っておくと便利な、Mutexによるプロセス間の同期や、スレッドタイマの利用方法などについて解説します。

対象読者

 この記事はパート1パート2の続きですので、パート1からお読みください。

必要な環境

 サンプルはVisual Studio .NET 2003で作成し、.NET Framework 1.1で動作確認をしています。

Monitor.Wait・Pluseメソッド

 MonitorクラスのWaitPulsePulseAllメソッドは、Monitor.EnterExitで同期されたコードのブロック内で使用します。あるスレッドがWaitを呼び出すとオブジェクトのロックが解放され、他のスレッドがPulseまたはPulseAllを呼び出し、再びロックが取得できるまでブロックされます。

 何はともあれ、具体的なサンプルを見てみましょう。サンプル「monitor_01」を見れば、Monitor.WaitPulseがどのような働きをするか分かります。

「VB.NET/monitor_01/Class1.vb」 (VB.NET)
Class Class1
    Private Shared syncObject As New Object

    'エントリポイント
    Public Shared Sub Main()
        '2つのスレッドを作成し、開始する
        Dim t1 As New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf A))
        t1.Name = "A"
        Dim t2 As New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf B))
        t2.Name = "B"
        t1.Start()
        t2.Start()

        Console.ReadLine()
    End Sub

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

        SyncLock syncObject
            Console.WriteLine("{0}:lock内に進入", _
                System.Threading.Thread.CurrentThread.Name)

            System.Threading.Thread.Sleep(2000)

            Console.WriteLine("{0}:Wait開始", _
                System.Threading.Thread.CurrentThread.Name)
            System.Threading.Monitor.Wait(syncObject)
            Console.WriteLine("{0}:Wait終了", _
                System.Threading.Thread.CurrentThread.Name)

            Console.WriteLine("{0}:lockから抜ける", _
                System.Threading.Thread.CurrentThread.Name)
        End SyncLock

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

    Public Shared Sub B()
        Console.WriteLine("{0}:スレッド開始", _
            System.Threading.Thread.CurrentThread.Name)
        System.Threading.Thread.Sleep(1000)

        SyncLock syncObject
            Console.WriteLine("{0}:lock内に進入", _
                System.Threading.Thread.CurrentThread.Name)

            Console.WriteLine("{0}:Pulse開始", _
                System.Threading.Thread.CurrentThread.Name)
            System.Threading.Monitor.Pulse(syncObject)

            System.Threading.Thread.Sleep(1000)

            Console.WriteLine("{0}:lockから抜ける", _
                System.Threading.Thread.CurrentThread.Name)
        End SyncLock

        Console.WriteLine("{0}:スレッド終了", _
            System.Threading.Thread.CurrentThread.Name)
    End Sub
End Class
「C#/monitor_01/Class1.cs」 (C#)
class Class1
{
    private static readonly object syncObject = new object();

    //エントリポイント
    public static void Main()
    {
        //2つのスレッドを作成し、開始する
        System.Threading.Thread t1 =
            new System.Threading.Thread(
            new System.Threading.ThreadStart(A));
        t1.Name = "A";
        System.Threading.Thread t2 =
            new System.Threading.Thread(
            new System.Threading.ThreadStart(B));
        t2.Name = "B";
        t1.Start();
        t2.Start();

        Console.ReadLine();
    }

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

        lock(syncObject)
        {
            Console.WriteLine("{0}:lock内に進入",
                System.Threading.Thread.CurrentThread.Name);

            System.Threading.Thread.Sleep(2000);

            Console.WriteLine("{0}:Wait開始",
                System.Threading.Thread.CurrentThread.Name);
            System.Threading.Monitor.Wait(syncObject);
            Console.WriteLine("{0}:Wait終了",
                System.Threading.Thread.CurrentThread.Name);

            Console.WriteLine("{0}:lockから抜ける",
                System.Threading.Thread.CurrentThread.Name);
        }

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

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

        lock(syncObject)
        {
            Console.WriteLine("{0}:lock内に進入",
                System.Threading.Thread.CurrentThread.Name);

            Console.WriteLine("{0}:Pulse開始",
                System.Threading.Thread.CurrentThread.Name);
            System.Threading.Monitor.Pulse(syncObject);

            System.Threading.Thread.Sleep(1000);

            Console.WriteLine("{0}:lockから抜ける",
                System.Threading.Thread.CurrentThread.Name);
        }

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

 このアプリケーションを実行すると、例えば次のように出力されます。

A:スレッド開始
A:lock内に進入
B:スレッド開始
A:Wait開始
B:lock内に進入
B:Pulse開始
B:lockから抜ける
B:スレッド終了
A:Wait終了
A:lockから抜ける
A:スレッド終了

 この例では、ThreadClassクラスのAメソッドとBメソッドを、別々のスレッドで実行します。まずAがクリティカルセクションに進入しますが、Monitor.Waitによりロックが解除されます。このことにより、Bでクリティカルセクションに進入できるようになります。次にBからMonitor.Pulseが呼び出されますが、これですぐにAのブロックが解除されるということはなく、Bがクリティカルセクションから抜けてロックが解放されてから、Aのブロックが解除されます。

ReaderWriterLockクラス

 複数のスレッドから共有リソースへのアクセスを同期するためにはlockを使用するのが基本ですが、読み取りアクセスに対しては複数のスレッドに許可を与え、書き込みアクセスに対しては一つのスレッドだけに許可を与えるという同期でよいのであれば、ReaderWriterLockクラスを使用した方がより効率的です。よって、共有リソースからの読み取りが頻繁で、書き込みはめったになく、短時間で終了するようなケースでは、ReaderWriterLockは有効な手段となります。

 サンプル「readerwriterlock_01」では、ReaderWriterLockクラスの基本的な使い方を紹介しています。

「VB.NET/readerwriterlock_01/Class1.vb」 (VB.NET)
Class Class1
    'ReaderWriterLockオブジェクトの作成
    Private Shared rwl As New System.Threading.ReaderWriterLock
    '複数スレッドからアクセスする共通リソース
    Private Shared resource As String = "0123456789"

    'エントリポイント
    Public Shared Sub Main()
        Dim t As System.Threading.Thread

        'スレッドを作成し、開始する
        '作成するスレッドのうち半分は'
        '共有リソースから読み取るメソッドを
        'もう半分は共有リソースに書き込むメソッドを実行する
        Dim i As Integer
        For i = 0 To 199
            If i Mod 2 = 0 Then
                t = New System.Threading.Thread( _
                    New System.Threading.ThreadStart( _
                    AddressOf ReadFromResource))
                t.Start()
            Else
                t = New System.Threading.Thread( _
                    New System.Threading.ThreadStart( _
                    AddressOf WriteToResource))
                t.Start()
            End If
        Next i

        Console.ReadLine()
    End Sub

    '共有リソースから読み込む
    Private Shared Sub ReadFromResource()
        'リーダーロックを取得
        rwl.AcquireReaderLock(System.Threading.Timeout.Infinite)

        '共有リソースからの読み取りがスレッドセーフ
        Console.WriteLine(resource)

        'ロックカウントをデクリメント
        '0になるとロックが解放される
        rwl.ReleaseReaderLock()
    End Sub

    '共有リソースに書き込む
    Private Shared Sub WriteToResource()
        'ライタロックを取得
        rwl.AcquireWriterLock(System.Threading.Timeout.Infinite)

        '共有リソースへの書き込み(読み取りも)がスレッドセーフ
        Dim s As String = resource.Substring(0, 1)
        resource = resource.Substring(1)
        System.Threading.Thread.Sleep(1)
        resource += s

        'ライタロックカウントをデクリメント
        '0になるとロックが解放される
        rwl.ReleaseWriterLock()
    End Sub
End Class
「C#/readerwriterlock_01/Class1.cs」 (C#)
class Class1
{
    //ReaderWriterLockオブジェクトの作成
    private static System.Threading.ReaderWriterLock rwl =
        new System.Threading.ReaderWriterLock();
    //複数スレッドからアクセスする共通リソース
    private static string resource = "0123456789";

    //エントリポイント
    public static void Main()
    {
        //スレッドを作成し、開始する
        //作成するスレッドのうち半分は
        //共有リソースから読み取るメソッドを
        //もう半分は共有リソースに書き込むメソッドを実行する
        for (int i = 0; i < 200; i++)
        {
            if (i % 2 == 0)
            {
                (new System.Threading.Thread(
                    new System.Threading.ThreadStart(
                    ReadFromResource))).Start();
            }
            else
            {
                (new System.Threading.Thread(
                    new System.Threading.ThreadStart(
                    WriteToResource))).Start();
            }
        }

        Console.ReadLine();
    }

    //共有リソースから読み込む
    private static void ReadFromResource()
    {
        //リーダーロックを取得
        rwl.AcquireReaderLock(System.Threading.Timeout.Infinite);

        //共有リソースからの読み取りがスレッドセーフ
        Console.WriteLine(resource);

        //ロックカウントをデクリメント
        //0になるとロックが解放される
        rwl.ReleaseReaderLock();
    }

    //共有リソースに書き込む
    private static void WriteToResource()
    {
        //ライタロックを取得
        rwl.AcquireWriterLock(System.Threading.Timeout.Infinite);

        //共有リソースへの書き込み(読み取りも)がスレッドセーフ
        string s = resource.Substring(0, 1);
        resource = resource.Substring(1);
        System.Threading.Thread.Sleep(1);
        resource += s;

        //ライタロックカウントをデクリメント
        //0になるとロックが解放される
        rwl.ReleaseWriterLock();
    }
}

 スレッドが読み取りのためのロック(リーダーロック)を取得する時は、ReaderWriterLock.AcquireReaderLockメソッドを、書き込みのためのロック(ライタロック)を取得する時は、ReaderWriterLock.AcquireWriterLockメソッドを呼び出します。つまり、共有リソースから読み込む箇所はAcquireReaderLockReleaseReaderLockで囲み、共有リソースに書き込む箇所はAcquireWriterLockReleaseWriterLockで囲むというのが、基本的な使い方となります。

Mutexクラス

 パート2の待機ハンドルの解説で触れたように、ここでMutex(ミューテックス)クラスについて説明します。Mutexクラスは、Monitor.EnterExitと同じような使い方ができます。まずはサンプル「mutex_01」をご覧ください。

「VB.NET/mutex_01/Class1.vb」 (VB.NET)
Class Class1
    Public Shared mut As System.Threading.Mutex

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

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

        Console.ReadLine()
    End Sub

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

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

            'Mutexを解放する
            mut.ReleaseMutex()
        Next i
    End Sub
End Class
「C#/mutex_01/Class1.cs」 (C#)
class Class1
{
    public static System.Threading.Mutex mut;

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

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

        Console.ReadLine();
    }

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

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

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

 この例では、DoSomethingメソッドのMutex.WaitOneMutex.ReleaseMutexで囲まれた箇所がクリティカルセクションとなります。Mutexは、同時に1つのスレッドでしか所有できない同期オブジェクトです。Mutexを使用して同期を行うには、次のようにします。

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

 Mutexの大きな特徴は、スレッド間の同期だけではなく、プロセス間の同期にも使用できることです。上記の例は同一プロセス内のスレッドを同期するものでしたが、実際にはこのようなケースではMonitorを使うべきであり、Mutexを使うべきではありません。Mutexはプロセス間の同期のみに使用するべきです。

 Mutexは、アプリケーションの二重起動を防ぐためによく使用されます。その例を以下に示します。

Mutexの利用例 (VB.NET)
Class Class1
    Private Shared _mutex As System.Threading.Mutex

    Shared Sub Main()
        'Mutexクラスの作成
        '"MyName"の部分を適当な文字列に変えてください
        _mutex = New System.Threading.Mutex(False, "MyName")

        'ミューテックスの所有権を要求する
        If _mutex.WaitOne(0, False) = False Then
            'すでに起動していると判断する
            Console.WriteLine("すでに起動しています。")
            '終了させるコードを書いてください
            Return
        End If

        Console.WriteLine("Enterキーで終了")
        Console.ReadLine()
    End Sub
End Class
Mutexの利用例 (C#)
class Class1
{
    private static System.Threading.Mutex _mutex;

    static void Main()
    {
        //Mutexクラスの作成
        //"MyName"の部分を適当な文字列に変えてください
        _mutex = new System.Threading.Mutex(false, "MyName");

        //ミューテックスの所有権を要求する
        if (_mutex.WaitOne(0, false) == false)
        {
            //すでに起動していると判断する
            Console.WriteLine("すでに起動しています。");
            //終了させるコードを書いてください
            return;
        }

        Console.WriteLine("Enterキーで終了");
        Console.ReadLine();
    }
}

 このアプリケーションを起動すると、「Enterキーで終了」と表示され、待機します。これを終了させずにもう一度アプリケーションを起動すると、「すでに起動しています。」と表示され、すぐに終了します。

 プロセス間でMutexを使用する場合は、適当なMutexの名前を指定する必要があります。名前の付け方に関して詳しくは、MSDNライブラリの「CreateMutex」をご覧ください。

補足
 この例で_mutexが静的フィールドとなっているのは、ガベージコレクションにより破棄されるのを防ぐためです。詳しくは、「.NET Tips - 二重起動を禁止する」をご覧ください。

スレッドセーフなコレクション

Synchronizedメソッド

 ArrayListQueueなどのコレクションクラスのインスタンスメンバは、スレッドセーフの保障がありません。ただし、ArrayListHashtableQueueSortedListStackクラスには、スレッドセーフにする方法が用意されています。その方法とは、Synchronizedメソッドを使ってスレッドセーフが保障されたラッパーを作成し、このラッパーを使用することです。

 ArrayListSortedListクラスは内容の読み取りはスレッドセーフですが、コレクションの内容が変更される場合は、スレッドセーフではありません。Hashtableクラスは読み取りに加え、単一の書き込み操作に関してはスレッドセーフです。

 サンプル「collection_01」では、ArrayListクラスのスレッドセーフなラッパーを作成する例を紹介しています。

「VB.NET/collection_01/Class1.vb」 (VB.NET)
'同期されていないArrayListオブジェクトの作成
Dim al As New System.Collections.ArrayList
'alを基にした同期されたArrayListラッパーの作成
Dim syncdAl As System.Collections.ArrayList = _
    System.Collections.ArrayList.Synchronized(al)
「C#/collection_01/Class1.cs」 (C#)
//同期されていないArrayListオブジェクトの作成
System.Collections.ArrayList al =
    new System.Collections.ArrayList();
//alを基にした同期されたArrayListラッパーの作成
System.Collections.ArrayList syncdAl =
    System.Collections.ArrayList.Synchronized(al);

 alはスレッドセーフではありませんが、syncAlはスレッドセーフとなります。スレッドセーフなラッパーをいきなり作成するには、次のようにすることもできます。

(VB.NET)
'同期されたArrayListの作成
syncdAl = System.Collections.ArrayList.Synchronized( _
    New System.Collections.ArrayList)
(C#)
//同期されたArrayListの作成
syncdAl =System.Collections.ArrayList.Synchronized(
    new System.Collections.ArrayList());
補足
 ArrayクラスのようにSynchronizedメソッドがないコレクションでは、lockを使用してスレッドセーフにします。

IsSynchronizedプロパティ

 サンプル「collection_01」では、IsSynchronizedプロパティも使用しています。

「VB.NET/collection_01/Class1.vb」 (VB.NET)
'syncdAlへのアクセスが同期されているか調べる
If syncdAl.IsSynchronized Then
    Console.WriteLine("syncdAlへのアクセスが同期されています")
Else
    Console.WriteLine("syncdAlへのアクセスが同期されていません")
End If
「C#/collection_01/Class1.cs」 (C#)
//syncdAlへのアクセスが同期されているか調べる
if (syncdAl.IsSynchronized)
{
    Console.WriteLine("syncdAlへのアクセスが同期されています");
}
else
{
    Console.WriteLine("syncdAlへのアクセスが同期されていません");
}

 IsSynchronizedメソッドを使うことにより、そのコレクションへのアクセスが同期されているか調べることができます。サンプル「collection_01」では、「syncdAlへのアクセスが同期されています」と表示されます。

コレクションの列挙処理

 コレクションの列挙処理は、同期されたラッパーを使用してもスレッドセーフではありません。列挙処理をスレッドセーフに行うには、lock(VB.NETではSyncLock)を使用します。この時、コレクションのSyncRootプロパティを使ってロックします。

 サンプル「collection_02」では、lockを使わずにコレクションの列挙を行っています。そのため、このアプリケーションを実行すると、エラーが発生します。

 サンプル「collection_03」では、lockを使用してコレクションの列挙処理をスレッドセーフに行い、エラーが発生しないようにしています。

「VB.NET/collection_03/Class1.vb」 (VB.NET)
Class Class1
    Private Shared syncdAl As System.Collections.ArrayList

    'エントリポイント
    Public Shared Sub Main()
        '同期されたArrayListの作成
        syncdAl = System.Collections.ArrayList.Synchronized( _
            New System.Collections.ArrayList)

        'ArrayListに要素を追加する
        Dim i As Integer
        For i = 0 To 99
            syncdAl.Add(i)
        Next i

        'スレッドの作成
        Dim t As New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf MyThread))
        'スレッドの開始
        t.Start()

        'syncdAlを変更する
        syncdAl.RemoveAt(0)

        Console.ReadLine()
    End Sub

    Private Shared Sub MyThread()
        'syncdAlをロックする
        SyncLock syncdAl.SyncRoot
            '列挙処理をする
            Dim i As Integer
            For Each i In syncdAl
                Console.Write(i)
            Next i
        End SyncLock
    End Sub
End Class
「C#/collection_03/Class1.cs」 (C#)
private static System.Collections.ArrayList syncdAl;

//エントリポイント
public static void Main()
{
    //同期されたArrayListの作成
    syncdAl =System.Collections.ArrayList.Synchronized(
        new System.Collections.ArrayList());

    //ArrayListに要素を追加する
    for (int i = 0; i < 100; i++)
    {
        syncdAl.Add(i);
    }

    //スレッドの作成と開始
    System.Threading.Thread t =
        new System.Threading.Thread(
        new System.Threading.ThreadStart(MyThread));
    t.Start();

    //syncdAlを変更する
    syncdAl.RemoveAt(0);

    Console.ReadLine();
}

private static void MyThread()
{
    //syncdAlをロックする
    lock(syncdAl.SyncRoot)
    {
        //列挙処理をする
        foreach (int i in syncdAl)
        {
            Console.Write(i);
        }
    }
}

 なお、TextReaderTextWriterMatchGroupクラスにもSynchronizedメソッドが存在します。これらもコレクションのSynchronizedメソッドと同様に、同期されたラッパーを返します。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2007/06/09 02:33 ReaderWriterLockクラスの解説の書き間違いを修正。

著者プロフィール

  • どぼん!(ドボン!)

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

バックナンバー

連載:.NET Frameworkにおけるマルチスレッドプログラミングの基本
All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5