スレッドの同期 (C# プログラミング ガイド)
更新 : 2007 年 11 月
次の各セクションでは、マルチスレッド アプリケーションでリソースへのアクセスを同期するために使用できる機能とクラスについて説明します。
アプリケーションで複数のスレッドを使用する利点の 1 つは、各スレッドを非同期的に実行できる点にあります。Windows アプリケーションでは、これによって時間のかかるタスクをバックグラウンドで実行しながら、アプリケーションのウィンドウやコントロールを応答可能な状態に維持できます。サーバー アプリケーションでは、マルチスレッドを使用することにより、受け取った各要求を別個のスレッドで処理できるようになります。マルチスレッドを使用しない場合、前の要求が完全に満たされるまで、新しい要求はサービスを受けることができません。
ただし、スレッドでの処理が非同期であるために、ファイル ハンドル、ネットワーク接続、およびメモリなどのリソースへのアクセスを調整する必要が生じます。調整が行われないと、互いに別のスレッドの動作が認識できず、複数のスレッドが同時に同じリソースにアクセスしてしまうことになります。その結果、予期しないデータ破損が発生します。
整数のデータ型に対する単純な演算の場合は、Interlocked クラスのメンバを使用することにより、スレッドの同期を実現できます。その他のすべてのデータ型やスレッド セーフではないリソースについては、このトピックで説明する構成要素を使用しない限り、マルチスレッド処理を安全に実行することはできません。
マルチスレッド プログラミングの背景情報については、次を参照してください。
lock キーワード
lock キーワードを使用すると、他のスレッドからの割り込みを受けることなくコード ブロックを確実に最後まで実行できます。これは、コード ブロックの実行中に、特定のオブジェクトに対して同時に使用できないロックを取得することで実現されます。
lock ステートメントは、引数としてオブジェクトが渡される lock キーワードから始まり、その後に、一度に 1 つのスレッドだけが実行するコード ブロックが続きます。次に例を示します。
public class TestThreading
{
private System.Object lockThis = new System.Object();
public void Function()
{
lock (lockThis)
{
// Access thread-sensitive resources.
}
}
}
lock キーワードに渡される引数は、参照型に基づくオブジェクトである必要があり、ロックのスコープを定義するために使用されます。前の例では、関数の外部にオブジェクト lockThis への参照が存在しないため、ロックのスコープはこの関数に限定されます。この参照が存在していたら、ロックのスコープはそのオブジェクトまで拡張されていました。厳密には、lock に渡されるオブジェクトは、複数のスレッドで共有されるリソースを一意に識別するためだけに使用されるので、任意のクラスのインスタンスを使用できます。ただし実際には、このオブジェクトは、スレッドの同期が必要なリソースを表すのが普通です。たとえば、複数のスレッドがコンテナ オブジェクトを使用する場合、そのコンテナを lock キーワードに渡すと、lock に続く、同期されたコード ブロックがそのコンテナにアクセスできるようになります。他のスレッドが同じコンテナにアクセスする前にコンテナをロックすると、このオブジェクトへのアクセスを安全に同期できます。
一般に、public 型や、アプリケーションの制御が及ばないオブジェクト インスタンスはロックしないことをお勧めします。たとえば、インスタンスにパブリックにアクセスできる場合、lock(this) は問題となることがあります。ユーザーの制御が及ばないコードによってもこのオブジェクトがロックされる可能性があるからです。この場合、複数のスレッドが同じオブジェクトの解放を待機しているような場合にはデッドロック状態が発生することがあります。オブジェクトではなく、パブリックなデータ型をロックした場合も同じ理由から問題が生じることがあります。リテラル文字列は共通言語ランタイム (CLR: Common Language Runtime) のインターン プールに存在しているため、リテラル文字列をロックすることは特に危険です。つまり、プログラム全体では任意のリテラル文字列のインスタンスは 1 つしか存在しませんが、まったく同じオブジェクトが、実行中のすべてのアプリケーション ドメインのすべてのスレッド上のリテラルを表すためです。この結果、アプリケーション プロセスの任意の場所で同じ内容を持つ文字列をロックした場合、アプリケーション内のその文字列のすべてのインスタンスがロックされてしまいます。したがって、インターン プールに存在しないプライベート メンバまたはプロテクト メンバをロックすることをお勧めします。クラスによっては、ロック専用のメンバを提供するものもあります。たとえば、Array 型では SyncRoot が提供されます。多くのコレクション型でも、SyncRoot メンバが提供されます。
lock キーワードの詳細については、次を参照してください。
Monitor
lock キーワードと同様、Monitor クラスも複数のスレッドによるコード ブロックの同時実行を防ぎます。Enter メソッドは 1 つのスレッドにのみ後続のステートメントに進むことを許可します。他のすべてのスレッドは、実行中のスレッドが Exit を呼び出すまでブロックされます。これは lock キーワードを使用した場合とまったく同じ結果になります。実際、lock キーワードは Monitor クラスを使用して実装されます。次に例を示します。
lock (x)
{
DoSomething();
}
このようにすると、次の記述と同じ結果が得られます。
System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
通常、Monitor クラスを直接使用するよりも lock キーワードを使用することをお勧めします。この理由としては、lock の方が簡潔であるということと、lock を使用すると、保護されたコードが例外をスローした場合でも基になる Monitor が確実に解放されるということがあります。Monitor を確実に解放するには、finally キーワードを使用します。このキーワードを使用することにより、例外がスローされたかどうかに関係なく関連付けられているコード ブロックが実行されます。
Monitor の詳細については、「Monitorによる同期化の技術サンプル」を参照してください。
同期イベントと待機ハンドル
スレッド依存のコード ブロックが同時に実行されないようにするために lock や Monitor を使用することは有効ですが、このような構成要素だけでは、スレッド間でイベントをやりとりすることはできません。そこで、同期イベントが必要になります。これは、シグナル状態と非シグナル状態という 2 つの状態を持つオブジェクトで、これを使用することによりスレッドをアクティブにしたり中断したりできます。非シグナル状態の同期イベントを待機させることによってスレッドを中断できます。また、イベントの状態をシグナル状態に変更することによりスレッドをアクティブにできます。既にシグナル状態にあるイベントをスレッドが待機している場合、スレッドは遅延なしに実行され続けます。
同期イベントには、AutoResetEvent と ManualResetEvent の 2 種類があります。この 2 つで唯一違うのは、AutoResetEvent の場合、1 つのスレッドをアクティブにすると、シグナル状態から非シグナル状態に変化するという点です。逆に ManualResetEvent では、そのシグナル状態によって任意の数のスレッドをアクティブにでき、Reset が呼び出された場合のみ非シグナル状態に戻ります。
WaitOne、WaitAny、または WaitAll の待機メソッドのいずれかを呼び出すことによって、スレッドを待機させることができます。WaitHandle.WaitOne() は、単一のイベントがシグナル状態になるまでスレッドを待機させます。WaitHandle.WaitAny() は、指定した 1 つ以上のイベントがシグナル状態になるまでスレッドをブロックします。また、WaitHandle.WaitAll() は、指定したすべてのイベントがシグナル状態になるまでスレッドをブロックします。イベントは、その Set メソッドが呼び出されると、シグナル状態になります。
次の例では、Main 関数によってスレッドが作成され開始されます。新しいスレッドは WaitOne メソッドを使用してイベントを待機します。このスレッドは、Main 関数を実行しているプライマリ スレッドによってイベントがシグナル状態になるまで中断されます。イベントがシグナル状態になると、この補助スレッドに制御が戻ります。この場合は 1 つのスレッドだけをアクティブにするためにイベントを使用しているので、AutoResetEvent クラスと ManualResetEvent クラスのいずれも使用できます。
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 クラスよりも相互運用機能の遷移に必要な計算上の負荷が大きくなってしまいます。ミューテックスの使用例については、「ミューテックス」を参照してください。
関連項目
Visual C# .NET または Visual C# 2005 を使用して、マルチスレッド環境で共有リソースへのアクセスを同期する方法
HOW TO: Submit a Work Item to the Thread Pool by Using Visual C# .NET
Visual C# .NET または Visual C# 2005 を使用して、マルチスレッド環境で共有リソースへのアクセスを同期する方法