スレッドの同期 (C# および Visual Basic)

次の各セクションでは、マルチスレッド アプリケーションでリソースへのアクセスを同期するために使用できる機能とクラスについて説明します。

アプリケーションで複数のスレッドを使用する利点の 1 つは、各スレッドを非同期的に実行できる点にあります。 Windows アプリケーションでは、これによって時間のかかるタスクをバックグラウンドで実行しながら、アプリケーションのウィンドウやコントロールを応答可能な状態に維持できます。 サーバー アプリケーションでは、マルチスレッドを使用することにより、受け取った各要求を別個のスレッドで処理できるようになります。 マルチスレッドを使用しない場合、前の要求が完全に満たされるまで、新しい要求はサービスを受けることができません。

ただし、スレッドでの処理が非同期であるために、ファイル ハンドル、ネットワーク接続、およびメモリなどのリソースへのアクセスを調整する必要が生じます。 調整が行われないと、互いに別のスレッドの動作が認識できず、複数のスレッドが同時に同じリソースにアクセスしてしまうことになります。 その結果、予期しないデータ破損が発生します。

整数のデータ型に対する単純な演算の場合は、Interlocked クラスのメンバーを使用することにより、スレッドの同期を実現できます。 その他のすべてのデータ型やスレッド セーフではないリソースについては、このトピックで説明する構成要素を使用しない限り、マルチスレッド処理を安全に実行することはできません。

マルチスレッド プログラミングの背景情報については、次を参照してください。

lock キーワードと SyncLock キーワード

lock ステートメント (C#) と SyncLock ステートメント (Visual Basic) を使用すると、他のスレッドからの割り込みを受けることなくコード ブロックを確実に最後まで実行できます。 これは、コード ブロックの実行中に、特定のオブジェクトに対して同時に使用できないロックを取得することで実現されます。

lock ステートメントまたは SyncLock ステートメントには、引数としてオブジェクトが渡され、その後に、一度に 1 つのスレッドだけが実行するコード ブロックが続きます。 次に例を示します。

Public Class TestThreading
    Dim lockThis As New Object 

    Public Sub Process()
        SyncLock lockThis
            ' Access thread-sensitive resources. 
        End SyncLock 
    End Sub 
End Class
public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Process()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}

lock キーワードに渡される引数は、参照型に基づくオブジェクトである必要があり、ロックのスコープを定義するために使用されます。 前の例では、関数の外部にオブジェクト lockThis への参照が存在しないため、ロックのスコープはこの関数に限定されます。 この参照が存在していたら、ロックのスコープはそのオブジェクトまで拡張されていました。 厳密には、オブジェクトは、複数のスレッドで共有されるリソースを一意に識別するためだけに使用されるので、任意のクラスのインスタンスを使用できます。 ただし実際には、このオブジェクトは、スレッドの同期が必要なリソースを表すのが普通です。 たとえば、複数のスレッドがコンテナー オブジェクトを使用する場合、そのコンテナーを lock キーワードに渡すと、lock に続く、同期されたコード ブロックがそのコンテナーにアクセスできるようになります。 他のスレッドが同じコンテナーにアクセスする前にコンテナーをロックすると、このオブジェクトへのアクセスを安全に同期できます。

一般に、public 型や、アプリケーションの制御が及ばないオブジェクト インスタンスはロックしないことをお勧めします。 たとえば、インスタンスにパブリックにアクセスできる場合、lock(this) は問題となることがあります。ユーザーの制御が及ばないコードによってもこのオブジェクトがロックされる可能性があるからです。 この場合、複数のスレッドが同じオブジェクトの解放を待機しているような場合にはデッドロック状態が発生することがあります。 オブジェクトではなく、パブリックなデータ型をロックした場合も同じ理由から問題が生じることがあります。 リテラル文字列は共通言語ランタイム (CLR: Common Language Runtime) のインターン プールに存在しているため、リテラル文字列をロックすることは特に危険です。 つまり、プログラム全体では任意のリテラル文字列のインスタンスは 1 つしか存在しませんが、まったく同じオブジェクトが、実行中のすべてのアプリケーション ドメインのすべてのスレッド上のリテラルを表すためです。 この結果、アプリケーション プロセスの任意の場所で同じ内容を持つ文字列をロックした場合、アプリケーション内のその文字列のすべてのインスタンスがロックされてしまいます。 したがって、インターン プールに存在しないプライベート メンバーまたはプロテクト メンバーをロックすることをお勧めします。 クラスによっては、ロック専用のメンバーを提供するものもあります。 たとえば、Array 型では SyncRoot が提供されます。 多くのコレクション型でも、SyncRoot メンバーが提供されます。

lock ステートメントと SyncLock ステートメントの詳細については、次のトピックを参照してください。

Monitor

lock キーワードおよび SyncLock キーワードと同様、Monitor クラスも複数のスレッドによるコード ブロックの同時実行を防ぎます。 Enter メソッドは 1 つのスレッドにのみ後続のステートメントに進むことを許可します。他のすべてのスレッドは、実行中のスレッドが Exit を呼び出すまでブロックされます。 これは lock キーワードを使用した場合とまったく同じ結果になります。 次に例を示します。

SyncLock x
    DoSomething()
End SyncLock
lock (x)
{
    DoSomething();
}

このようにすると、次の記述と同じ結果が得られます。

Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
    DoSomething()
Finally
    System.Threading.Monitor.Exit(obj)
End Try
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

通常、Monitor クラスを直接使用するよりも lock キーワード (C#) または SyncLock キーワード (Visual Basic) を使用することをお勧めします。この理由としては、lock または SyncLock の方が簡潔であるということと、lock または SyncLock を使用すると、保護されたコードが例外をスローした場合でも基になる Monitor が確実に解放されるということがあります。 Monitor を確実に解放するには、finally キーワードを使用します。このキーワードを使用することにより、例外がスローされたかどうかに関係なく関連付けられているコード ブロックが実行されます。

同期イベントと待機ハンドル

スレッド依存のコード ブロックが同時に実行されないようにするために lock や Monitor を使用することは有効ですが、このような構成要素だけでは、スレッド間でイベントをやりとりすることはできません。 そこで、同期イベントが必要になります。これは、シグナル状態と非シグナル状態という 2 つの状態を持つオブジェクトで、これを使用することによりスレッドをアクティブにしたり中断したりできます。 非シグナル状態の同期イベントを待機させることによってスレッドを中断できます。また、イベントの状態をシグナル状態に変更することによりスレッドをアクティブにできます。 既にシグナル状態にあるイベントをスレッドが待機している場合、スレッドは遅延なしに実行され続けます。

同期イベントには、AutoResetEventManualResetEvent の 2 種類があります。 この 2 つで唯一違うのは、AutoResetEvent の場合、1 つのスレッドをアクティブにすると、シグナル状態から非シグナル状態に変化するという点です。 逆に ManualResetEvent では、そのシグナル状態によって任意の数のスレッドをアクティブにでき、Reset が呼び出された場合のみ非シグナル状態に戻ります。

WaitOneWaitAnyWaitAll など、いずれかの待機メソッドを呼び出すことによって、スレッドを待機させることができます。 WaitHandle.WaitOne は、単一のイベントがシグナル状態になるまでスレッドを待機させます。WaitHandle.WaitAny は、指定した 1 つ以上のイベントがシグナル状態になるまでスレッドをブロックします。また、WaitHandle.WaitAll は、指定したすべてのイベントがシグナル状態になるまでスレッドをブロックします。 イベントは、その Set メソッドが呼び出されると、シグナル状態になります。

次の例では、Main 関数によってスレッドが作成され開始されます。 新しいスレッドは WaitOne メソッドを使用してイベントを待機します。 このスレッドは、Main 関数を実行しているプライマリ スレッドによってイベントがシグナル状態になるまで中断されます。 イベントがシグナル状態になると、この補助スレッドに制御が戻ります。 この場合は 1 つのスレッドだけをアクティブにするためにイベントを使用しているので、AutoResetEvent クラスと ManualResetEvent クラスのいずれも使用できます。

Imports System.Threading

Module Module1
    Dim autoEvent As AutoResetEvent

    Sub DoWork()
        Console.WriteLine("   worker thread started, now waiting on event...")
        autoEvent.WaitOne()
        Console.WriteLine("   worker thread reactivated, now exiting...")
    End Sub 

    Sub Main()
        autoEvent = New AutoResetEvent(False)

        Console.WriteLine("main thread starting worker thread...")
        Dim t As New Thread(AddressOf DoWork)
        t.Start()

        Console.WriteLine("main thread sleeping for 1 second...")
        Thread.Sleep(1000)

        Console.WriteLine("main thread signaling worker thread...")
        autoEvent.Set()
    End Sub 
End Module
using System;
using System.Threading;

class ThreadingExample
{
    static AutoResetEvent autoEvent;

    static void DoWork()
    {
        Console.WriteLine("   worker thread started, now waiting on event...");
        autoEvent.WaitOne();
        Console.WriteLine("   worker thread reactivated, now exiting...");
    }

    static void Main()
    {
        autoEvent = new AutoResetEvent(false);

        Console.WriteLine("main thread starting worker thread...");
        Thread t = new Thread(DoWork);
        t.Start();

        Console.WriteLine("main thread sleeping for 1 second...");
        Thread.Sleep(1000);

        Console.WriteLine("main thread signaling worker thread...");
        autoEvent.Set();
    }
}

Mutex オブジェクト

ミューテックスは、複数のスレッドによってコード ブロックが同時に実行されるのを防ぐという点で Monitor に似ています。 実際、"mutex (ミューテックス)" という名前は、"mutually exclusive (同時に指定できない)" という用語の短縮形です。ただし、Monitor とは違って、ミューテックスを使用するとプロセス間でスレッドを同期できます。 ミューテックスは、Mutex クラスによって表されます。

プロセス間での同期を行うために使用されるミューテックスは、名前付きミューテックスと呼ばれます。このようなミューテックスは別のアプリケーションで使用される可能性があるため、グローバル変数や静的変数を使用して共有できないからです。 したがって、両方のアプリケーションから同じミューテックス オブジェクトにアクセスできるように、名前を付ける必要があります。

ミューテックスを使用するとプロセス間でスレッドを同期できますが、通常は Monitor の使用をお勧めします。その理由は、Monitor が .NET Framework 専用にデザインされているため、より適切にリソースを利用できる点にあります。 一方、Mutex クラスは Win32 の構成要素のラッパーです。 ミューテックスは Monitor よりも強力ですが、Monitor クラスよりも相互運用機能の遷移に必要な計算上の負荷が大きくなってしまいます。 ミューテックスの使用例については、「ミューテックス」を参照してください。

Interlocked クラス

Interlocked クラスのメソッドを使用すると、複数のスレッドが同じ値を同時に更新または比較しようとするときに発生する問題を回避できます。 このクラスのメソッドによって、どのスレッドの値も安全にインクリメント、デクリメント、交換、比較することができます。

ReaderWriter ロック

場合によっては、データを書き込むときだけリソースをロックし、データが更新されないときには複数のクライアントにデータの同時読み取りを許可する場合があります。 ReaderWriterLock クラスを使用すると、スレッドがリソースを変更する間はリソースに排他アクセスを適用し、リソースを読み取るときには排他的でないアクセスを許可できます。 ReaderWriter ロックは、データの更新が必要ないときでも、他のスレッドを待たせる方法として、排他ロックに代わる有効な手段です。

デッドロック

スレッドの同期は、マルチスレッド アプリケーションにとって非常に大切ですが、deadlock を生じさせてしまう危険性が常にあります。つまり、複数のスレッドが互いに待機しあって、アプリケーションが中断してしまう状態です。 デッドロックをたとえて言えば、四方向一時停止の交差点で停止した車どうしが、相手の車を先に行かせるよう互いに譲り合い、どちらも動けなくなっている状態と同じです。 デッドロックを防ぐことは重要です。その鍵となるのは、綿密なプランです。 コーディングを始める前にマルチスレッド アプリケーションを図式化すると、デッドロック状態を予想できることがよくあります。

関連項目

方法: スレッド プールを使用する (C# および Visual Basic)

Visual C# .NET または Visual C# 2005 を使用して、マルチスレッド環境で共有リソースへのアクセスを同期する方法

Visual C# を使用してスレッドを作成する方法

Visual C# を使用してスレッド プールに作業項目を送信する方法

Visual C# .NET または Visual C# 2005 を使用して、マルチスレッド環境で共有リソースへのアクセスを同期する方法

参照

関連項目

SyncLock ステートメント

lock ステートメント (C# リファレンス)

Thread

WaitOne

WaitAny

WaitAll

Join

Start

Sleep

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

EventWaitHandle

System.Threading

Set

概念

マルチスレッド アプリケーション (C# および Visual Basic)

ミューテックス

監視

インタロックされた操作

AutoResetEvent

マルチスレッド処理のためのデータの同期

その他の技術情報

CLR の非同期プログラミング モデルを実装する

C# で簡素化された APM

デッドロック モニター

コンポーネントのマルチスレッド

Visual C# .NET または Visual C# 2005 を使用して、マルチスレッド環境で共有リソースへのアクセスを同期する方法