マルチスレッドには、慎重なプログラミングが必要です。 ほとんどのタスクでは、スレッド プール スレッドによる実行要求をキューに入れるので、複雑さを軽減できます。 このトピックでは、複数のスレッドの作業の調整やブロックするスレッドの処理など、より困難な状況に対処します。
注
.NET Framework 4 以降、タスク並列ライブラリと PLINQ には、マルチスレッド プログラミングの複雑さとリスクの一部を軽減する API が用意されています。 詳細については、「 .NET での並列プログラミング」を参照してください。
デッドロックと競合状態
マルチスレッドはスループットと応答性に関する問題を解決しますが、そうすることで、デッドロックや競合状態という新しい問題が発生します。
デッドロック
デッドロックは、2 つのスレッドのそれぞれが、もう一方のスレッドが既にロックされているリソースをロックしようとしたときに発生します。 どちらのスレッドもそれ以上の進行状況を行う必要はありません。
マネージド スレッド クラスの多くのメソッドは、デッドロックの検出に役立つタイムアウトを提供します。 たとえば、次のコードは、 lockObject
という名前のオブジェクトに対するロックの取得を試みます。 ロックが 300 ミリ秒で取得されない場合、 Monitor.TryEnter は 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.
}
競合状態
競合状態は、プログラムの結果が、2 つ以上のスレッドのうち、最初に特定のコード ブロックに到達したスレッドに依存するときに発生するバグです。 プログラムを何度も実行すると異なる結果が生成され、特定の実行の結果を予測することはできません。
競合状態の簡単な例は、フィールドをインクリメントすることです。 クラスに、objCt++;
(C#) や objCt += 1
(Visual Basic) などのコードを使用して、クラスのインスタンスが作成されるたびにインクリメントされるプライベート静的フィールド (Visual Basic で共有) があるとします。 この操作では、 objCt
からレジスタに値を読み込み、値をインクリメントして、 objCt
に格納する必要があります。
マルチスレッド アプリケーションでは、値を読み込んでインクリメントしたスレッドが、3 つの手順をすべて実行する別のスレッドによって割り込まれる可能性があります。最初のスレッドが実行を再開してその値を格納すると、中間で値が変更されたことを考慮せずに objCt
が上書きされます。
この特定の競合状態は、 Interlocked クラスのメソッド ( Interlocked.Increment など) を使用して簡単に回避できます。 複数のスレッド間でデータを同期するその他の手法については、「 マルチスレッドのためのデータの同期」を参照してください。
競合状態は、複数のスレッドのアクティビティを同期するときにも発生する可能性があります。 コード行を記述するときは常に、行を実行する前 (または行を構成する個々のマシン命令の前に) スレッドが割り込まれた場合に何が起こるかを考慮する必要があります。また、別のスレッドがその行を超過した場合も考慮する必要があります。
静的メンバーと静的コンストラクター
クラス コンストラクター (C# ではコンストラクターstatic
、Visual Basic では Shared Sub New
) の実行が完了するまで、クラスは初期化されません。 初期化されていない型でコードが実行されないようにするために、共通言語ランタイムは、クラス コンストラクターの実行が完了するまで、他のスレッドからクラスのメンバー (Visual Basic のメンバーShared
) をstatic
へのすべての呼び出しをブロックします。
たとえば、クラス コンストラクターが新しいスレッドを開始し、スレッド プロシージャがクラスの static
メンバーを呼び出す場合、新しいスレッドはクラス コンストラクターが完了するまでブロックします。
これは、 static
コンストラクターを持つ可能性がある任意の型に適用されます。
プロセッサの数
複数のプロセッサがあるかどうか、またはシステムで使用可能なプロセッサが 1 つだけであるかに関係なく、マルチスレッド アーキテクチャに影響を与える可能性があります。 詳細については、「 プロセッサの数」を参照してください。
実行時に使用可能なプロセッサの数を決定するには、 Environment.ProcessorCount プロパティを使用します。
一般的な推奨事項
複数のスレッドを使用する場合は、次のガイドラインを考慮してください。
Thread.Abortを使用して他のスレッドを終了しないでください。 別のスレッドで
Abort
を呼び出すことは、そのスレッドが処理で到達したポイントを知らずに、そのスレッドで例外をスローするのと同じものです。Thread.SuspendとThread.Resumeを使用して複数のスレッドのアクティビティを同期しないでください。 Mutex、ManualResetEvent、AutoResetEvent、およびMonitorを使用してください。
メイン プログラムからのワーカー スレッドの実行を制御しないでください (イベントなどを使用)。 代わりに、ワーカー スレッドが作業が利用可能になるまで待機し、実行し、完了したらプログラムの他の部分に通知するようにプログラムを設計します。 ワーカー スレッドがブロックしない場合は、スレッド プール スレッドの使用を検討してください。 Monitor.PulseAll は、ワーカー スレッドがブロックされる状況で役立ちます。
ロック オブジェクトとして型を使用しないでください。 つまり、C# での
lock(typeof(X))
や Visual Basic でのSyncLock(GetType(X))
などのコードや、Type オブジェクトでのMonitor.Enterの使用は避けてください。 特定の種類の場合、アプリケーション ドメインごとに System.Type のインスタンスは 1 つだけです。 ロックを行う型がパブリックの場合、独自のコード以外のコードがロックを受け取り、デッドロックが発生する可能性があります。 その他の問題については、「 信頼性のベスト プラクティス」を参照してください。C# での
lock(this)
や Visual Basic でのSyncLock(Me)
など、インスタンスをロックする場合は注意が必要です。 型の外部にあるアプリケーション内の他のコードがオブジェクトをロックすると、デッドロックが発生する可能性があります。スレッドがモニターに入っている間に例外が発生した場合でも、モニターに入ったスレッドが常にそのモニターから離れるようにしてください。 C# lock ステートメントと Visual Basic SyncLock ステートメントは、この動作を自動的に提供し、 finally ブロックを使用して、 Monitor.Exit が確実に呼び出されるようにします。 Exit が呼び出されることを確認できない場合は、Mutex を使用するようにデザインを変更することを検討してください。 ミューテックスは、現在所有しているスレッドが終了すると自動的に解放されます。
異なるリソースを必要とするタスクには複数のスレッドを使用し、1 つのリソースに複数のスレッドを割り当てないようにします。 たとえば、I/O を含むタスクは、そのスレッドが I/O 操作中にブロックされ、他のスレッドの実行が許可されるため、独自のスレッドを持つことの利点があります。 ユーザー入力は、専用スレッドの利点を持つもう 1 つのリソースです。 単一プロセッサ コンピューターでは、集中的な計算を伴うタスクは、ユーザー入力と I/O を伴うタスクと共存しますが、複数の計算集中型タスクは互いに競合します。
lock
ステートメント (Visual Basic でSyncLock
) を使用する代わりに、単純な状態変更にInterlocked クラスのメソッドを使用することを検討してください。lock
ステートメントは汎用ツールとして適していますが、Interlocked クラスはアトミックである必要がある更新プログラムのパフォーマンスを向上させます。 内部的には、競合がない場合は 1 つのロック プレフィックスを実行します。 コード レビューでは、次の例に示すようなコードを監視します。 最初の例では、状態変数がインクリメントされます。SyncLock lockObject myField += 1 End SyncLock
lock(lockObject) { myField++; }
次のように、
lock
ステートメントの代わりに Increment メソッドを使用してパフォーマンスを向上させることができます。System.Threading.Interlocked.Increment(myField)
System.Threading.Interlocked.Increment(myField);
注
1 より大きいアトミックインクリメントには、 Add メソッドを使用します。
2 番目の例では、参照型変数が null 参照である場合にのみ更新されます (Visual Basic では
Nothing
)。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 メソッドを使用してパフォーマンスを向上させることができます。
System.Threading.Interlocked.CompareExchange(x, y, Nothing)
System.Threading.Interlocked.CompareExchange(ref x, y, null);
注
CompareExchange<T>(T, T, T) メソッドのオーバーロードは、参照型の型セーフな代替手段を提供します。
クラス ライブラリの推奨事項
マルチスレッド用のクラス ライブラリを設計する場合は、次のガイドラインを考慮してください。
可能であれば、同期の必要性を避けてください。 これは特に、頻繁に使用されるコードに当てはまります。 たとえば、アルゴリズムを排除するのではなく、競合状態を許容するように調整できます。 不要な同期によってパフォーマンスが低下し、デッドロックや競合状態が発生する可能性があります。
既定では、静的データ (Visual Basic で
Shared
) スレッドを安全にします。既定では、インスタンス データ スレッドを安全にしないでください。 スレッド セーフなコードを作成するためのロックを追加すると、パフォーマンスが低下し、ロックの競合が増加し、デッドロックが発生する可能性が生じます。 一般的なアプリケーション モデルでは、一度に 1 つのスレッドのみがユーザー コードを実行するため、スレッド セーフの必要性が最小限に抑えられます。 このため、.NET クラス ライブラリは既定ではスレッド セーフではありません。
静的な状態を変更する静的メソッドを指定しないでください。 一般的なサーバー シナリオでは、静的状態は要求間で共有されます。つまり、複数のスレッドがそのコードを同時に実行できます。 これにより、スレッドのバグが発生する可能性が高くなります。 要求間で共有されないインスタンスにデータをカプセル化する設計パターンの使用を検討してください。 さらに、静的データが同期されている場合、状態を変更する静的メソッド間の呼び出しによってデッドロックや冗長な同期が発生し、パフォーマンスに悪影響を与える可能性があります。
こちらも参照ください
.NET