安全でないコンテナの使用を検知する
この例をMicrosoft Visual Studio .NET 2005でコンパイルし、4プロセッサのIntel Xeonサーバー上で4つのスレッドによって実行してください。そうすれば、何か問題があることがすぐに分かるでしょう。31行目によって報告される合計値が実行のたびに変わり、決してN(1000000)と等しくなりません。競合状態やその他のスレッド化エラーがあるのかもしれません。
この点を確認するため、潜在的なデータ競合やデッドロックやその他のスレッド化エラーを検出するツール(Intel Thread Checker)でこの例を実行してみました。Intel Thread Checkerにより、スレッド間の潜在的な依存関係を検知し、その依存関係をエラーの元になっているソース行にマップすることができました。これらの潜在的な依存関係は、すべて同じ箇所(8行目)を指していました。これは、スレッドがSTLのmapにアクセスするところです(図1を参照)。
スケーラブルなソリューションを見つける
図1を見ると、mapへのアクセスを同期化する必要があることが明らかになります。まず、単一のWin32 MutexまたはCRITICAL_SECTIONオブジェクトを使ってアクセスを監視する処理のパフォーマンスを調べます。既に述べたように、粒度の粗い同期は、スレッドセーフでないライブラリを使ってスレッドセーフを保証する唯一の方法です。
Win32 Mutexは全プロセスから見えるカーネルオブジェクトです。単一のMutexオブジェクトでSTLのmapへの各アクセスを監視すれば、スレッドセーフが保証されますが、そうするとオーバーヘッドも非常に大きくなります。それに対し、Win32 CRITICAL_SECTIONは軽量で、ユーザー空間のプロセス内ミューテックスオブジェクトなので、こちらの方が好ましい選択肢です。
図2に、STLのmapを使って同期化を行うバージョンのパフォーマンスを、同じコードをシーケンシャルに実装した場合を1として示します。
同期がなければ、STLのmapは良好なパフォーマンスをもたらしますが、同時アクセスが行われると問題が起き、誤った結果を引き起こします。Win32 Mutexはコストが高いため、予想どおり劣悪なパフォーマンスをもたらします。単一のCRITICAL_SECTIONオブジェクトと粒度の粗い同期を用いた場合も、パフォーマンスは低下します。スレッドセーフなSTL mapベースの実装は、どちらも元のシーケンシャルな実装よりも実行時間が長くなります。粒度の粗い同期は正確さを復活させますが、同時並行性がないためスケーラビリティが低下します。
図3は、STLのmapをTBBのconcurrent_hash_mapに置き換えたときのパフォーマンスを示しています。
concurrent_hash_mapを使用する場合のパフォーマンスと、STLのmapで同期を行わない場合のパフォーマンスとを比較すると、Intel TBBによって実現されるスレッドセーフにはそれなりに代償が伴うことが明らかになります。しかしこの実装は、他のスレッドセーフな実装とは異なり、1よりも大きなスピードアップをもたらすだけの同時並行性を実現します。
つまり、TBBの並列コンテナの同時並行性を利用すると、安全でないコンテナに粒度の粗いロックを適用する方法ではとても実現できない高いパフォーマンスが可能になります。
これからはますます多くの開発者が、マルチコアシステムへの移行という避けがたい事態に直面することになるでしょう。こうした開発者にとっては、マルチスレッドアプリケーション開発でエラーを起きにくくするためのツールを探求することが絶対必要になります。TBBに含まれている並列コンテナクラスもそうしたものの1つであり、マルチコアへの移行を容易にしてくれます。これらの並列コンテナクラスが提供する安全でスケーラブルな実装のおかげで、C++ STLで定義されているようなスレッドフレンドリでないコンテナを使わずに済み、このようなコンテナに伴う諸々の問題を回避できるからです。



