マネージド スレッド処理のベスト プラクティスManaged threading best practices

マルチスレッドには慎重なプログラミングが必要です。Multithreading requires careful programming. ほとんどのタスクでは、スレッド プールのスレッドを使って実行の要求をキューに置くことによって、処理の複雑さを軽減できます。For most tasks, you can reduce complexity by queuing requests for execution by thread pool threads. このトピックでは、マルチ スレッド動作の調整や、ブロックするスレッドの処理など、より難しい状況について説明します。This topic addresses more difficult situations, such as coordinating the work of multiple threads, or handling threads that block.

注意

.NET Framework 4 以降では、マルチスレッド プログラミングの複雑さとリスクを軽減する API が Task Parallel Library および PLINQ に用意されています。Starting with the .NET Framework 4, the Task Parallel Library and PLINQ provide APIs that reduce some of the complexity and risks of multi-threaded programming. 詳細については、.NET の並列プログラミングに関するページをご覧ください。For more information, see Parallel Programming in .NET.

デッドロックと競合状態Deadlocks and race conditions

マルチスレッドはスループットと応答速度の問題を解決しますが、その一方で、デッドロックと競合状態という新たな問題を発生させます。Multithreading solves problems with throughput and responsiveness, but in doing so it introduces new problems: deadlocks and race conditions.

デッドロックDeadlocks

デッドロックは、2 つのスレッドのうちの一方が、もう一方によって既にロックされているリソースをロックしようとすると発生します。A deadlock occurs when each of two threads tries to lock a resource the other has already locked. こうなると、どちらのスレッドも続行できなくなります。Neither thread can make any further progress.

マネージド スレッド処理クラスの多くのメソッドには、ロックアウトを検出するためのタイムアウト機能が用意されています。Many methods of the managed threading classes provide time-outs to help you detect deadlocks. たとえば、lockObject というオブジェクトへのロックの取得を試みるコードを次に示します。For example, the following code attempts to acquire a lock on an object named lockObject. ロックが 300 ミリ秒の間に得られない場合は、Monitor.TryEnterfalse を返します。If the lock is not obtained in 300 milliseconds, Monitor.TryEnter returns false.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}  

競合状態Race conditions

競合状態は、2 つ以上のスレッドのうちのどれが特定のコード ブロックに最初に到達するかによって、プログラムの結果が変わってしまうバグのことです。A race condition is a bug that occurs when the outcome of a program depends on which of two or more threads reaches a particular block of code first. プログラムを何回か実行すると、異なる結果が得られ、実行の結果は予測できません。Running the program many times produces different results, and the result of any given run cannot be predicted.

競合状態の簡単な例として、フィールドのインクリメントがあります。A simple example of a race condition is incrementing a field. クラスにプライベートの static フィールド (Visual Basic では Shared) があり、このフィールドは、objCt++; (C# の場合) または objCt += 1 (Visual Basic の場合) のようなコードを使用して、クラスのインスタンスが生成されるたびにインクリメントされるものとします。Suppose a class has a private static field (Shared in Visual Basic) that is incremented every time an instance of the class is created, using code such as objCt++; (C#) or objCt += 1 (Visual Basic). この演算によって、objCt からレジスタへの値の読み込み、値のインクリメント、および objCt への値の格納が行われます。This operation requires loading the value from objCt into a register, incrementing the value, and storing it in objCt.

マルチスレッドされたアプリケーションでは、値の読み込みとインクリメントを行ったスレッドが、この 3 つの処理を実行する別のスレッドに先を越される可能性があります。最初のスレッドは、実行を再開して値を格納するとき、待機中に値が変更されたかどうかを考慮せずに、objCt の値を上書きしてしまいます。In a multithreaded application, a thread that has loaded and incremented the value might be preempted by another thread which performs all three steps; when the first thread resumes execution and stores its value, it overwrites objCt without taking into account the fact that the value has changed in the interim.

このような特定の競合状態は、Interlocked など、Interlocked.Increment クラスのメソッドを使用することによって容易に回避できます。This particular race condition is easily avoided by using methods of the Interlocked class, such as Interlocked.Increment. 複数のスレッド間でデータを同期する他の手法については、「マルチスレッド処理のためのデータの同期」を参照してください。To read about other techniques for synchronizing data among multiple threads, see Synchronizing Data for Multithreading.

競合状態は、複数のスレッドの動作を同期するときにも発生します。Race conditions can also occur when you synchronize the activities of multiple threads. コードを記述するときには、そのコードの行 (または行を構成する各マシン命令) を実行するスレッドが別のスレッドに追い越された場合に何が起きるのかを考慮しておく必要があります。Whenever you write a line of code, you must consider what might happen if a thread were preempted before executing the line (or before any of the individual machine instructions that make up the line), and another thread overtook it.

静的メンバーと静的コンストラクターStatic members and static constructors

クラスは、そのクラス コンストラクター (C# では static コンストラクター、Visual Basic では Shared Sub New) の実行が完了するまで初期化されません。A class is not initialized until its class constructor (static constructor in C#, Shared Sub New in Visual Basic) has finished running. 初期化されていない型のコードの実行を防止するため、共通言語ランタイムは、クラス コンストラクターの実行が完了するまで、他のスレッドからのそのクラスの static メンバー (Visual Basic では Shared メンバー) 呼び出しをすべてブロックします。To prevent the execution of code on a type that is not initialized, the common language runtime blocks all calls from other threads to static members of the class (Shared members in Visual Basic) until the class constructor has finished running.

たとえば、クラス コンストラクターが新しいスレッドを起動し、そのスレッドのプロシージャがクラスの static メンバーを呼び出した場合、その新しいスレッドは、クラス コンストラクターが完了するまでブロックされます。For example, if a class constructor starts a new thread, and the thread procedure calls a static member of the class, the new thread blocks until the class constructor completes.

このことは、static コンストラクターを持つことができるすべての型に当てはまります。This applies to any type that can have a static constructor.

プロセッサの数Number of processors

システムで利用できるプロセッサの数 (複数か 1 つだけか) がマルチスレッド アーキテクチャに影響を与えることがあります。Whether there are multiple processors or only one processor available on a system can influence multithreaded architecture. 詳細については、「プロセッサの数」を参照してください。For more information, see Number of Processors.

実行時に利用できるプロセッサの数を判断するには Environment.ProcessorCount プロパティを使用します。Use the Environment.ProcessorCount property to determine the number of processors available at runtime.

一般的な推奨事項General recommendations

マルチスレッドを使用するときは、以下のガイドラインを考慮してください。Consider the following guidelines when using multiple threads:

  • 他のスレッドを終了させるために Thread.Abort を使用することは避けてください。Don't use Thread.Abort to terminate other threads. 他のスレッド Abort を呼び出すことは、そのスレッドの処理がどこまで到達しているかを把握せずに例外をスローするのと同じことになります。Calling Abort on another thread is akin to throwing an exception on that thread, without knowing what point that thread has reached in its processing.

  • Thread.SuspendThread.Resume を使用して複数のスレッドの動作を同期することは避けてください。Don't use Thread.Suspend and Thread.Resume to synchronize the activities of multiple threads. MutexManualResetEventAutoResetEvent、および Monitor を使用してください。Do use Mutex, ManualResetEvent, AutoResetEvent, and Monitor.

  • メイン プログラムから、イベントなどを使用して、ワーカー スレッドの実行を制御しないでください。Don't control the execution of worker threads from your main program (using events, for example). ワーカー スレッドの側で、作業ができるようになるまでの待機、作業の実行、および実行終了後のプログラムへの通知を行うように、プログラムをデザインします。Instead, design your program so that worker threads are responsible for waiting until work is available, executing it, and notifying other parts of your program when finished. ワーカー スレッドによるブロックを行わない場合は、スレッド プールのスレッドを使用することを考慮します。If your worker threads do not block, consider using thread pool threads. ワーカー スレッドがブロックを行う場合は、Monitor.PulseAll が役立ちます。Monitor.PulseAll is useful in situations where worker threads block.

  • ロック オブジェクトとして型を使用しないでください。Don't use types as lock objects. つまり、C# の lock(typeof(X))、Visual Basic の SyncLock(GetType(X)) などのコードを使用すること、または Monitor.EnterType オブジェクトと組み合わせて使用することを避けます。That is, avoid code such as lock(typeof(X)) in C# or SyncLock(GetType(X)) in Visual Basic, or the use of Monitor.Enter with Type objects. System.Type のインスタンスは、1 つの型につきアプリケーション ドメインごとに 1 つのみです。For a given type, there is only one instance of System.Type per application domain. ロックする型がパブリックの場合、自分以外のコードでもその型をロックできるため、デッドロックになる可能性があります。If the type you take a lock on is public, code other than your own can take locks on it, leading to deadlocks. 詳細については、「信頼性に関するベスト プラクティス」を参照してください。For additional issues, see Reliability Best Practices.

  • インスタンスをロックする場合 (たとえば、C# の lock(this)、Visual Basic の SyncLock(Me)) には注意してください。Use caution when locking on instances, for example lock(this) in C# or SyncLock(Me) in Visual Basic. アプリケーション内の、その型以外の他のコードがオブジェクトをロックすると、デッドロックが発生する場合があります。If other code in your application, external to the type, takes a lock on the object, deadlocks could occur.

  • モニター状態に入った (ロックを取得した) スレッドは、モニター状態である間に例外が発生した場合でも、必ずモニター状態から出すようにします。Do ensure that a thread that has entered a monitor always leaves that monitor, even if an exception occurs while the thread is in the monitor. C# の lock ステートメントと Visual Basic の SyncLock ステートメントは、finally ブロックを使用して Monitor.Exit が呼び出されるようにすることで、この動作を自動的に提供します。The C# lock statement and the Visual Basic SyncLock statement provide this behavior automatically, employing a finally block to ensure that Monitor.Exit is called. Exit を確実に呼び出すことができない場合は、Mutex を使用するようにデザインを変更することを検討してください。If you cannot ensure that Exit will be called, consider changing your design to use Mutex. ミューテックスは、現在それを保持しているスレッドが終了すると、自動的に解放されます。A mutex is automatically released when the thread that currently owns it terminates.

  • マルチ スレッドは異なるリソースを必要とするタスクに使用し、1 つのリソースに複数のスレッドを割り当てることがないように注意します。Do use multiple threads for tasks that require different resources, and avoid assigning multiple threads to a single resource. たとえば、I/O を含む作業であれば、その作業専用のスレッドを用意すると有益です。I/O 操作の間、このスレッドはブロックを行いますが、他のスレッドは実行できるからです。For example, any task involving I/O benefits from having its own thread, because that thread will block during I/O operations and thus allow other threads to execute. ユーザー入力も、専用のスレッドが役に立つリソースの 1 つです。User input is another resource that benefits from a dedicated thread. シングル プロセッサのコンピューターでは、計算中心のタスクがユーザー入力や I/O タスクと共存することはできますが、計算中心タスクどうしは競合してしまいます。On a single-processor computer, a task that involves intensive computation coexists with user input and with tasks that involve I/O, but multiple computation-intensive tasks contend with each other.

  • 単純な状態変更については、Interlocked ステートメント (Visual Basic では lock) ではなく、SyncLock クラスのメソッドの使用を検討します。Consider using methods of the Interlocked class for simple state changes, instead of using the lock statement (SyncLock in Visual Basic). lock ステートメントは汎用的なツールとして優れていますが、Interlocked クラスは、分離不可能な状態であることが必要な更新のパフォーマンスに優れています。The lock statement is a good general-purpose tool, but the Interlocked class provides better performance for updates that must be atomic. 内部的には、競合がない場合は、単一の lock プレフィックスが実行されます。Internally, it executes a single lock prefix if there is no contention. コードの校閲で、次に示す例のようなコードを探します。In code reviews, watch for code like that shown in the following examples. 最初の例では、状態変数をインクリメントしています。In the first example, a state variable is incremented:

    SyncLock lockObject  
        myField += 1  
    End SyncLock  
    
    lock(lockObject)   
    {  
        myField++;  
    }  
    

    Increment ステートメントの代わりに lock メソッドを次のように使用すると、パフォーマンスを向上できます。You can improve performance by using the Increment method instead of the lock statement, as follows:

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    注意

    .NET Framework 2.0 以降では、1 より大きいアトミック インクリメントに対して Add メソッドを使用します。In the .NET Framework 2.0 and later, use the Add method for atomic increments larger than 1.

    2 番目の例では、参照型の変数を、null 参照 (Visual Basic では Nothing) の場合にのみ更新しています。In the second example, a reference type variable is updated only if it is a null reference (Nothing in Visual Basic).

    If x Is Nothing Then  
        SyncLock lockObject  
            If x Is Nothing Then  
                x = y  
            End If  
        End SyncLock  
    End If  
    
    if (x == null)  
    {  
        lock (lockObject)  
        {  
            x ??= y;
        }  
    }  
    

    この代わりに CompareExchange メソッドを次のように使用すると、パフォーマンスを向上できます。Performance can be improved by using the CompareExchange method instead, as follows:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    注意

    .NET Framework 2.0 より、CompareExchange<T>(T, T, T) メソッド オーバーロードから参照型に対してタイプ セーフの代替が提供されます。Beginning with .NET Framework 2.0, the CompareExchange<T>(T, T, T) method overload provides a type-safe alternative for reference types.

クラス ライブラリに関する推奨事項Recommendations for class libraries

マルチスレッド用のクラス ライブラリをデザインするときには、次のガイドラインを検討します。Consider the following guidelines when designing class libraries for multithreading:

  • 可能な限り、同期の必要を避けるようにします。Avoid the need for synchronization, if possible. これは、頻繁に使用するコードの場合に特に言えます。This is especially true for heavily used code. たとえば、競合状態をなくすのではなく、競合状態に対応できるようにアルゴリズムを調整できる場合があります。For example, an algorithm might be adjusted to tolerate a race condition rather than eliminate it. 不要な同期があると、パフォーマンスが低下し、デッドロックや競合状態が発生する可能性が生じます。Unnecessary synchronization decreases performance and creates the possibility of deadlocks and race conditions.

  • 静的なデータ (Visual Basic では Shared) は既定でスレッド セーフにします。Make static data (Shared in Visual Basic) thread safe by default.

  • インスタンス データは既定でスレッド セーフにしないようにします。Do not make instance data thread safe by default. スレッド セーフなコードを作成するロックを追加すると、パフォーマンスが低下し、ロックの競合が増加し、デッドロックが発生する可能性が生じます。Adding locks to create thread-safe code decreases performance, increases lock contention, and creates the possibility for deadlocks to occur. 一般的なアプリケーション モデルでは、一度にユーザー コードを実行するスレッドは 1 つだけにして、スレッド セーフを実現する必要を最小限に抑えます。In common application models, only one thread at a time executes user code, which minimizes the need for thread safety. この理由から、.NET Framework のクラス ライブラリは既定ではスレッド セーフではないことが必要です。For this reason, the .NET Framework class libraries are not thread safe by default.

  • 静的状態を変更する静的メソッドは提供しないでください。Avoid providing static methods that alter static state. 一般的なサーバーのシナリオでは、静的状態は要求間で共有されます。つまり、複数のスレッドがそのコードを同時に実行できます。In common server scenarios, static state is shared across requests, which means multiple threads can execute that code at the same time. これにより、スレッド処理のバグが発生する可能性が高くなります。This opens up the possibility of threading bugs. 要求間で共有されないインスタンスにデータをカプセル化するデザイン パターンの使用を検討してください。Consider using a design pattern that encapsulates data into instances that are not shared across requests. 加えて、静的なデータを同期する場合は、状態を変更する呼び出しが静的メソッド間にあると、デッドロックや冗長な同期が生じる可能性があり、パフォーマンスに悪影響を及ぼします。Furthermore, if static data are synchronized, calls between static methods that alter state can result in deadlocks or redundant synchronization, adversely affecting performance.

関連項目See also