印刷用ページ       送信     
クリックして評価とフィードバックをお寄せください
 同時実行 : 同時実行の問題を特定するためのツールと手法
Related Articles

今月の記事では、非同期操作の実行順序に依存関係を適用し、便利な DependencyManagement クラスを作成する方法を Stephen Toub が説明します。

Stephen Toub

MSDN Magazine April 2009

...

Read more!

今月の .NET の問題では、コラムニストの Stephen Toub が非同期 I/O に関する読者からの質問に答えます。

Stephen Toub

MSDN Magazine July 2008

...

Read more!

次期バージョンの Visual Studio でマネージ コードとネイティブ コードの両方に対して計画されている並列プログラミングのサポートを見ていきます。

Stephen Toub and Hazim Shafi

MSDN Magazine October 2008

...

Read more!

この記事では、同時実行プログラミングのいくつかの一般的な課題について説明し、ソフトウェアでそれらの課題に対処するための助言を提供します。

Joe Duffy

MSDN Magazine October 2008

...

Read more!

この記事では、F# 言語が、他の任意の .NET 準拠言語からシームレスに呼び出すことができる非同期関数ライブラリの作成にどのように役立つかを解説しています。

Chance Coble

MSDN Magazine October 2008

...

Read more!

Popular Articles

Kenny Kerr 氏は、Visual C++ を現代的かつ便利に使用できるようになる Visual C++ 2008 Feature Pack を絶賛しています。

Kenny Kerr

MSDN Magazine May 2008

...

Read more!

James Avery does it again with his popular list of developer tools. This time he covers the best Visual Studio add-ins available today that you can download for free.

James Avery

MSDN Magazine December 2005

...

Read more!

The MVP pattern helps you separate your logic and keep your UI layer free of clutter. This month learn how.

Jean-Paul Boodhoo

MSDN Magazine August 2006

...

Read more!

This article introduces 10 development tools that can increase your productivity, give you a better understanding of .NET, and maybe even change the way that you develop applications. The tools covered include NUnit to write unit tests, Reflector to examine assemblies, FxCop to police your code, Regulator to build regular expressions, NDoc to create code documentation and five more.

James Avery

MSDN Magazine July 2004

...

Read more!

Microsoft patterns & practices の Composite Application Guidance for WPF で複合アプリケーションを作成する利点を紹介します。

Glenn Block

MSDN Magazine September 2008

...

Read more!

同時実行
同時実行の問題を特定するためのツールと手法
Rahul V. Patil および Boby George

この記事では、次の内容について説明します。
  • 同時実行のテストにおける問題点
  • 見つけにくいバグとその依存関係
  • 障害を検出するための新しいツール
  • テスト設計のベスト プラクティス
この記事では、次のテクノロジを使用しています。
動的分析と静的分析、モデル チェック
コンピュータの処理能力の向上は、永遠の課題です。この課題に対処するため、ハードウェア業界は、マルチコアおよびメニーコア プロセッサ システムに着実にシフトしつつあります。アプリケーションのパフォーマンスはクロック速度の速い高速のプロセッサを搭載することで改善できますが、メニーコア システムのパフォーマンスを高めるには、効率の良い並列プログラムを記述するしかありません。
ソフトウェア業界には、随分以前から並列処理という考え方がありました。ただし、並列ハードウェアの能力を最大限に活用するメインストリームのソフトウェア アプリケーションを開発するには、逐次アプリケーション向けの従来の慣習を大きく変える必要があります。
並列アプリケーションのテストは容易ではありません。たとえば、並列アプリケーションには非確定的な動作があるため、同時実行に関するバグの検出は難しくなります。これらのバグを検出できたとしても、同じバグを一貫して再現するのは困難です。また、問題を修正した後で、問題が本当に修正されたのか、それとも単に見えなくなっただけなのかを見極めるのも簡単ではありません。そのうえ、並列化によって、特定すべきパフォーマンス ボトルネックが新たに生まれることもあります。
この記事では、並列プログラム向けのテスト手法について説明し、危険性を秘めた欠陥の特定に使用できる 6 つの有用なツールを紹介します。最初に、3 つのカテゴリ (競合状態、不適切な相互排除、およびメモリの並べ替え) の同時実行に関するバグについて説明します。

競合状態、デッドロック、その他
競合は、マルチスレッド プログラムの複数の実行スレッドが同じ共有データにアクセスしたときに、その中の少なくとも 1 つのアクセスが書き込みアクセスである場合に発生します。不適切な競合状態は予期しない動作につながりますが、多くの場合、その状態を検出することは容易ではありません。競合状態の影響は、大分時間がたってから明らかになることもあれば、プログラムのまったく別の箇所に現れることもあります。そのうえ、再現がきわめて困難です。スレッド間で処理を適切に順序付けする同期の手法を使用すると、競合を避けることができます。
テスト データとレポート メトリックの特定
並列アプリケーションのテスト データを定義するには、シナリオのスケーラビリティの側面を明らかにする必要があります。アプリケーションのスケーラビリティは、使用されるアルゴリズムの並列化可能性に大きく左右されます。特定の問題領域 (分布が不均一なコンテンツの検索など) において超線形の速度向上を実現するアルゴリズムもあれば、並列化可能性に限界があるために並列的な速度向上がそれほどでもなく、全体としての実行時速度が CPU の数に比例しないようなアルゴリズムもあります。さらに、処理速度がデータセットのサイズに比例しないアルゴリズム (ソート アルゴリズムなど) もあります。おわかりのように、次のカテゴリの要素によってアプリケーションのパフォーマンスがどのように変化するかを理解することが重要です。
スレッドや CPU コアの数 ユーザーは、通常、CPU やスレッドの数が増えると、それに比例して並列アプリケーションのパフォーマンスも向上するものと考えています。
データセット サイズ ユーザーは、通常、入力データセットのサイズが大きくなっても、並列アプリケーションのパフォーマンスは低下しないものと考えています。
処理ワークロード 処理が進むと、スレッドによる作業の量が一定に保たれなくなることがあります。ワークロードは、増える場合も減る場合もあります。また、ランダムに変化することも、正規分布の形で増減することもあります。処理の進行に伴うワークロードの変化は、アプリケーションのパフォーマンス特性に影響を及ぼす可能性があります。こうしたワークロードの変化のタイプには、増加、減少、ランダムな変化、正規分布の形での増減などがあります。
レポートに使用される最も一般的なメトリックは、実行時間です。それ以外のレポート メトリックとしては、逐次アプリケーションとの比較により明らかになる速度の差、スケーラビリティのさまざまな要素を適用したときの速度向上などがあります。重要なのは、レポート メトリックとその測定方法を定義することです。まず、どのような値をレポートする必要があるのか、その値はどのような方法でレポートするのか、測定に使用する機器はパフォーマンスに影響を及ぼす可能性があるか、などを検討することから始めてください。また、可能であれば、パーツごとに測定を行うことをお勧めします。システムのサブパーツのパフォーマンスを把握できるようにするためです。
競合は、安全で意図的なものである場合もあります。たとえば、"done" というグローバル フラグがあり、それに対してライタが 1 つ、リーダーが多数存在するケースを考えてみましょう。ライタ スレッドはフラグを設定することで、すべてのスレッドに対して安全に終了するよう伝えます。このとき、すべてのリーダー スレッドは while (!done) によってループ状態で実行され、繰り返しフラグを読み続けている可能性があります。done フラグが設定されていることを認識したスレッドは、while ループから抜けます。ほとんどの場合、これは問題のない競合と言えます。この記事の後半で説明するように、競合状態の検出に使用できるツールがいくつか存在します。ただし、そのようなツールは、実際には問題のない競合を検出し、誤検知と呼ばれるエラーを報告することがあります。
デッドロックは、複数のスレッドが互いの処理を待ち、循環を形成して、すべてのスレッドの処理が進行しなくなる場合に発生します。プログラマは、競合状態を避けようとして、かえってデッドロックを生み出すことがあります。たとえば、同期プリミティブを適切に使用しないと (ロックが適切な順序で取得されない場合など)、複数のスレッドが互いの処理を待機することがあります。デッドロックは、ロック構造を使用していない場合にも発生することがあります。つまり、どのような循環待機もデッドロックにつながる可能性があると言えます。
次の例に、デッドロックが発生する可能性のある状態を示します。スレッド 1 は次の行を実行します。
Acquire Lock A;
Acquire Lock B; 
Release Lock B; 
Release Lock A;
Thread 2 proceeds like so:
Acquire Lock B;
Acquire Lock A; // if thread 1 has already acquired lock 
                // A and waiting to acquire lock B, then 
                // this is a deadlock
Release Lock A;
Release Lock B;
停止状態とは、マルチスレッド アプリケーションの 1 つまたは複数の実行可能なスレッドが無期限に遅延している状態か、永続的にブロックされている状態です。ブロックされておらず、何らかの処理を待機している状態でなくても、そのスレッドに実行される見込みがない場合は、停止状態にあると言います。通常、停止状態は、ルールやポリシーのスケジュールの結果です。たとえば、優先順位が高く、非ブロックで、継続的に実行されるスレッドが、優先順位の低いスレッドと共にスケジュールされている場合、シングルコア CPU では優先順位の低いスレッドは実行されなくなります。こうした状況を避けるために、Windows® スケジューラが頻繁に介入し、停止状態にあるスレッドの優先順位を必要に応じて引き上げることで、停止状態を抑制しています。
ライブロックが発生するのは、スレッドが互いの状態の変化にその都度反応するために、スケジュールされているにもかかわらず処理が進まなくなる場合です。この状態を最もよく表すのが、狭い廊下で対面した 2 人の人物が同じ方向によけることで、お互いの行く手をふさぎ続けているような場面です。こうした動作によって処理の進行が妨げられ、結果的にライブロック状態に陥ります。実際に処理が行われているようすがないのに CPU の使用率が高い状態は、ライブロックの典型的な危険信号です。ライブロックの検出や診断はきわめて困難です。

メモリ オーダリングに起因する問題
コンパイラがコードを最適化しているときに、パフォーマンスを高めるためにコードの一部を並べ替えることがあります。ところが、この並べ替えにより、グローバル状態の変化を監視しているスレッドに思わぬ動作が発生するおそれがあります。たとえば、スレッド 1 とスレッド 2 という 2 つの実行スレッドがあるとします。また、ゼロに初期化されている、x および y というグローバル変数もあると仮定します。スレッド 1 は次の行を実行します。
x=10;
y=5;
スレッド 2 は次の行を実行します。
if (y == 5)
{
  Assert(x != 10);
}
このとき、最適化コンパイラは、y には最終的に 5 が再代入されることになると判断し、コードの順序を変更して、x に 10 を代入する前に y に 5 を代入する場合があります (こうした 2 回の代入を行う単一のスレッドという観点からは、オーダリングは問題になりません)。したがって、スレッド 2 のアサートが実行される可能性があります。x が 10 に等しくない場合があるためです。
必要に応じて、volatile 変数とコンパイラ組み込みを使用し、コンパイラによるメモリの並べ替えが行われないようにすることができます。また、これも重要な情報ですが、共通の最適化フラグを有効にした場合、スレッド 2 は変数の y または x の更新を確認しなくなります。その場合は、volatile 変数を使用する必要があります。
コンパイラによる並べ替え以外にも、厄介な問題があります。メモリ アクセスの最適化 (投機実行によるキャッシュや高度なフェッチなど) により、ある CPU のあるスレッドによって実行されたメモリ操作が、別の CPU の別のスレッドによって異なる順序で行われた操作として判断されることがあります。このようにメモリ操作が認識されるのは、複数の CPU または複数の CPU コアを搭載したシステムに限られます。
具体的に考えてみましょう。スレッド 1 およびスレッド 2 という 2 つの実行スレッドがあると仮定します。さらに、m と n はゼロに初期化されたグローバル変数であり、a、b、c、および d はローカル変数であるとします。(プロセッサ 1 上の) スレッド 1 には次の命令があります。
m = 5;     // A1
int a = m; // A2
int b = n; // A3
(プロセッサ 2 上の) スレッド 2 には次の命令があります。
n = 5;     //A4
int c = n; //A5
int d = m; //A6
論理的には (処理が一貫して順番に行われるメモリでは)、これらの命令がすべて実行された後に b == d == 0 になることはあり得ません。ただし、ハードウェア メモリの並べ替えが行われると、共有メモリ (A1 および A4) への書き込みがストアバッファに送られ、各プロセッサによる書き込みの認識に違いが生じることがあります。

テスト戦略
並列アプリケーションのテストを行う場合は、正確性、信頼性、パフォーマンス、およびスケーラビリティをテストする必要があります。アプリケーションの正確性は、静的分析、動的分析、モデル チェックなどの手法を使用して見極めます。パフォーマンスと信頼性をテストするには、並列処理によって生じるオーバーヘッドを調査し、ストレス テストを実施します。スケーラビリティをテストするには、さまざまなサイズのシステムでアプリケーションがどの程度のパフォーマンスを発揮するかを分析します。
静的分析の手法では、コードを分析する際、実際にはプログラムは実行されません。静的分析では、コンパイルされたアプリケーションや注釈付きソース コードのメタデータを調べるのが普通です。一般に、静的分析では、プログラマの意図と想定によって不適切な動作が抑えられていることを確認するために、形式的な検査も行われます。PREfix と PREfast の 2 つのツールは、ネイティブ アプリケーションの分析に最も広く使用されている静的分析ツールです。また、FxCop はマネージ コードの分析に広く使用されています。
静的分析には長所と短所があります。長所としては、カバレッジの広さと細かさ、製品設計の信頼性 (形式的な分析がビルドに役立ちます)、バグの検出と修正を容易にする正確なエラー レポートなどがあります。その一方で、静的分析には、同時実行に関するバグを適切に処理するために、意図を示す注釈が大量に必要になります。しかも、その注釈自体に正確性が求められます。また、静的分析を使用するツールには誤検知が多い傾向があるため、誤検知を最小限に抑える作業に多大な労力を費やさなければなりません。
動的分析では、実行のフットプリントを調べてバグの検出を行います。動的分析には、オンライン分析とオフライン分析の 2 種類があります。オンライン動的分析を使用するツールでは、実行中のプログラムを分析します。一方、オフライン動的分析を使用するツールでは、トレースを記録し、後からそれを分析してバグを検出します。開発者がプログラミングの時点で不要な作業を行う必要がほとんどないうえ、誤検知も少なく、明確な問題に対処しやすくなるので、動的分析は利用しやすい手法です。バグの検出は実行パスだけで行われるので、パスを頻繁にスキャンすることで一連の重大なバグを見つけることができます。そのため、信頼性を簡単に高めることができます。
ただし、短所もあります。バグの検出は実行パスだけで行われるので、コード カバレッジを広げるために、テスト ケースを使用する必要があるのです。事実、コードを実行したときに発生した競合しか検出できないようなツールもあります。したがって、ツールによるバグの報告がなくても、バグがないという確証を得ることはできません。ほとんどの動的分析ツールは、実行時の動作を変更できる何らかのインストルメンテーションに依存していますが、これも動的分析ツールの短所です。これらのツールは複雑なので、パフォーマンスが非常に低い傾向にあります。
最後の手法は、有限状態並列システムの正確性を検証するためのモデル チェックです。この手法では、形式的な演繹法を利用できます。モデル チェッカーは競合とデッドロック状態のシミュレーションを試みます。モデル チェックを行うと、競合とデッドロックがないことを形式的に証明することができます。この手法には、設計とアーキテクチャの信頼性を大幅に高めることができ、カバレッジが非常に広く、外部のドライバが最小限で済むという長所があります。
他のテスト手法と同様に、モデル チェックにもいくつかの短所があります。ほとんど場合、コードから自動的にモデルを抽出することが非常に困難です (限定的な利用の場合はまれに可能です)。モデルを手動で抽出するのもきわめて面倒です。状態空間の肥大化 (起こり得る状態の数が分析しきれないほど多い状態) のため、検証にも長時間を要します。状態空間の肥大化は、複雑なヒューリスティックを利用する抑制手法を適用することである程度は制御可能ですが、そのようなヒューリスティックには、長時間実行アプリケーションのバグを見逃すというマイナス面もあります。
さらに、モデル チェックを行うには、設計と実装に関する綿密な計画が必要になります。モデル チェックによって設計にエラーがないことが証明されても、実装には依然として問題が残されているようなケースもあります。実際のところ、モデル チェックが役立つのは、製品の重要な一部分を調べる場合に限られます。その他、動的分析とモデル チェックを組み合わせるハイブリッドな手法もあります。その一例が CHESS ですが、これについては後ほど説明します。
競合検出アルゴリズム
静的分析ツールと動的分析ツールの両方で使用されるロックセット アルゴリズムは、複数のスレッドが共通のロックを持たない状態で共有サービスにアクセスした場合に発生する可能性のある競合を報告します。基本的に、このアルゴリズムでは、各共有メモリ変数 v の空でないロック セット C(v) は、その変数にアクセスしているスレッドによって保持されます。当初は、C(v) はすべてのロックの一覧です。各スレッドは、locks(t) (保持されているすべてのロック) と writeLocks(t) (保持されているすべての書き込みロック) の 2 つのロック セットも保持します。次に、アルゴリズムによる処理方法を示します。
  1. 各共有メモリ変数 v に対し、ロック セット C(v) を保持します。当初、C(v) はすべてのロックの一覧です。
  2. 各スレッドは、locks(t) (保持されているすべてのロック) と writeLocks(t) (保持されているすべての書き込みロック) の 2 つのロック セットも保持します。
  3. スレッド t による v の各読み取り :
    1. C(v) = C(v) ∩ locks(t) と設定。
    2. C(v) == NULL セットの場合、エラーを生成。
  4. スレッド t による v の各書き込み :
    1. C(v) = C(v) ∩ writeLocks(t) と設定。
    2. C(v) == NULL セットの場合、エラーを生成。
基本的に、アプリケーションの処理が進むと各変数の C(v) は小さくなります。また、C(v) が null になるとエラーが発生します (たとえば、共有メモリへのアクセス時にスレッドのロックセットの積集合が NULL の場合)。
残念ながら、ロックセット アルゴリズムによって報告される競合は、必ずしも実際の競合であるとは限りません。巧みなプログラミング トリックを利用するか、他の同期プリミティブ (シグナルなど) を使用すると、ロックを使用せずに競合の起こらないコードを記述することもできます。ただし、このような手法を使用すると本物のバグの検出が非常に困難になります。この問題を軽減するうえで役立つのが、注釈や特定の抑制です。
競合を検出するためのアルゴリズムとしては、分散システムでのイベントの半順序に基づく "happens-before (事前発生)" アルゴリズムもあります。ここでは、あるイベントの前にどのイベントが発生したかを特定するために半順序を計算するアルゴリズムについて説明します (この文脈でのイベントとは、読み取り、書き込み、およびロックを含めたすべての命令のことです)。
  • 単一のスレッド内では、イベントは発生した順番で配列されます。
  • スレッド間では、イベントは同期プリミティブのプロパティに従って配列されます。たとえば、2 つのスレッドが lock(a) を取得しようとしている場合、一方のスレッドによるロックの解除は、もう一方のスレッドによるロックの "前に発生した (happened-before)" と言います。
  • 複数のスレッドが共有変数にアクセスした場合に、それらのアクセスが "happens-before (事前発生)" の関係によって確定的に順序付けされないとき、競合が発生したと言います。
図 A にこのアルゴリズムを示します。
スレッド 1 のロック解除はスレッド 2 のロックの前に発生しています。したがって、共有変数へのアクセスは同時には起こり得ず、競合はありません。このアルゴリズムの大きな短所は、こうした関係の監視による負荷が非常に大きくなる場合があるという点です。
さらに問題なのは、このアルゴリズムの競合検出能力がスケジュールの順序に完全に依存している点です。半順序は特定のスケジュール インスタンスに対して構成されるため、同じテストを別の日に行うと、バグが見過ごされる可能性があります。図 B の場合は競合は報告されませんが、図 C の実行順序の場合は競合が報告されます。製品がリリースされてから何年もたって初めて発生する競合が数多くあることが知られています。そのため、この検出アルゴリズムを使用しても、ありとあらゆるバグを検出できた、という安心感を得ることはできません。
その一方で、"happens-before (事前発生)" アルゴリズムには誤検知が非常に少なく、検出されるバグのほとんどが実際に問題のあるバグです。ただし、"happens-before (事前発生)" アルゴリズムは多数のエラーを見逃すため (false negative)、効率的に実装するのは非常に困難です。
それと同じように、ロックセット アルゴリズムは非常に効率が良く、多数のエラーを検出できますが、誤検知 (false positive) が非常に多い傾向にあります。そのため、これらのアルゴリズムを組み合わせることで両アプローチの弱点を克服しようという取り組みが続けられています。注 : これらの競合検出アルゴリズムには、ここ数年の間に改良が加えられています。その詳細については、ACM/IEEE のポータルを参照してください。
図 A "Happens-Before (事前発生)" アルゴリズム (クリックすると拡大画像が表示されます)
図 B 競合は報告されない (クリックすると拡大画像が表示されます)
図 C 競合が検出される (クリックすると拡大画像が表示されます)

同時実行テスト ツール
デッドロック、ライブロック、ハングなど、並列処理の際に起こり得るあらゆる問題への対処に役立つ同時実行テスト ツールは、多数市販されています。ここで紹介するツールは、いずれも特定の領域で力を発揮するものです。
CHESS: Microsoft Research が作成した CHESS は、モデル チェックと動的分析を組み合わせた斬新なツールです (go.microsoft.com/fwlink/?LinkId=116523 を参照してください)。このツールは、体系的にスレッド スケジュールを調べ、インタリーブすることで、同時実行のエラーを検出します。競合状態、デッドロック、ハング、ライブロック、およびデータ破損の問題を検出する機能を備えています。また、デバッグを支援するために、実行は完全に反復可能になっています。ほとんどのモデル チェックの場合と同じく、体系的な検証が行われるため、完全なカバレッジが実現されます。
動的分析ツールとしての CHESS は、専用のスケジューラを使用して、定期的な単体テストを繰り返します。実行のたびに異なるスケジュール順序が選択されます。モデル チェッカーとしては、特定のスレッド インタリーブを作成する機能を備えた専用のスケジューラを制御します。状態空間の肥大化を抑制するために、CHESS は半順序法と新しい反復コンテキスト範囲を利用します。
CHESS の反復コンテキスト範囲では、深度で状態空間の肥大化を制限する代わりに、特定の実行におけるスレッドの切り替え数を制限します。切り替えまでの間にスレッド自体で実行できるステップの数に制限はなく、実行深度にも制限は加えられません (これは、従来のモデル チェックから大きく改良された点です)。このメカニズムは、同時実行に関するほとんどのバグは、少数の体系的なスレッド切り替えだけで明らかにできるという経験的証拠に基づいています。
CHESS ではデッドロックと競合を検出できますが (go.microsoft.com/fwlink/?LinkId=116877 で説明されている Goldilocks ロックセット アルゴリズムを使用)、他の状態を検証する場合は、プログラマによるアサーションが必要になります。また、すべてのプログラムを終了する必要があると同時に、すべてのスレッドの公平性が保証されている (処理が進行する) 必要もあります。そのため、プログラムが連続したループに入ると、ライブロックが報告されます。
テストという観点からすると、反復コンテキスト範囲を 2 に設定して CHESS を実行することをお勧めします。バグが検出されなくなったら範囲を 3 にし、その後同様に 1 ずつ大きくしていきます。経験から言って、2 と 3 の範囲でほとんどのバグを検出できます。したがって、バグを短時間で除去するうえで非常に効率的な手法と言えます。
インストルメンテーションのために、CHESS は Win32® API 同期呼び出しを迂回させて、非確定性を抑制したり、意図的に組み込んだりします。また、CHESS を使用する場合は、状態の一貫性を確保するために、開発者コードに多数のアサートを含める必要があります (適切なコードは常にそうあるべきですが)。ただし、ほとんどの動的分析ツールと同様に、広範なカバレッジを実現するには適切なテスト スイートが必要になります。インタリーブ テストの質を高めるため、これまでは負荷を利用するしかなかった開発者やテスト担当者に、CHESS は非常に人気があります。CHESS では、定期的に単体テストを行い、注目すべきインタリーブを体系的にシミュレートすることができます。
Intel Thread Checker: このツールは、デッドロック (発生する可能性のあるデッドロックも含む)、ストール、データ競合、およびネイティブ Windows 同期 API の不適切な使用を検出するための動的分析ツールです (go.microsoft.com/fwlink/?LinkId=115727 を参照してください)。Thread Checker では、すべてのメモリ参照と標準の Win32 同期プリミティブを識別できるようにするために、ソース コードまたはコンパイルされたバイナリをインストルメント化する必要があります。実行時に、そのインストルメント化されたバイナリから、アナライザが実行の半順序を構成するために必要な情報が提供されます。その後、半順序に基づいて "happens-before (事前発生)" 分析が行われます。"happens-before (事前発生)" 分析の詳細については、「競合検出アルゴリズム」を参照してください。
このツールは、パフォーマンスとスケーラビリティの理由から、共有変数に対するアクセスをすべて記憶することはせず、最近のアクセスだけを記憶します。そのため、長時間実行アプリケーションを分析する際のツールの効率が高まります。ただし、一部のバグを見逃すという副作用もあります。妥協が必要になりますが、実行時間が非常に短いアプリケーションのバグをすべて見つけることよりも、長時間実行アプリケーションのバグを多数見つけることの方が重要と言えます。
その他に、このツール唯一の大きな弱点は、カスタム スピン ロックで使用されるようなインタロック操作を通じた同期が考慮されていないという点です。ただし、このツールは、標準の同期プリミティブだけが使用されているアプリケーション用としては、ネイティブ アプリケーションの同時実行テストに利用できるテスト ツールの中でも最も支持されているツールの 1 つであると言えます。
RacerX このツールは、競合とデッドロックの検出に使用される、フローを識別する静的分析ツールです。このツールでは、ソース コード全体に大量の注釈を付けなければならないという問題が解消されています。実際に、注釈については、ロックの取得と解除に使用する API を指定したテーブルを提供するだけでかまいません。このテーブルには、スピン、ブロック、再入などのロック プリミティブの属性も指定できます。通常、このテーブルのサイズはごく小さく、含まれるエントリは多くても 30 程度です。そのため、大規模なシステムのソースに注釈を付ける負担が大幅に軽減されます。
最初の段階で、RacerX は各ソース コード ファイルを反復処理し、制御フロー グラフ (CFG) を作成します。CFG には、関数呼び出し、共有メモリ、ポインタの使用などのデータに関連する情報が含まれます。CFG を作成する際に、RacerX は同期プリミティブ テーブルの逆参照も行い、これらの API の呼び出しをマークします。
CFG が完成すると、分析段階に入ります。この段階では、競合チェッカーとデッドロック チェッカーが実行されます。CFG のスキャンには時間がかかることがありますが、適切な時間短縮とキャッシュの手法が使用されるため、影響は最小限に抑えられます。コンテキスト フローをスキャンする際にはロックセット アルゴリズムが使用され、発生する可能性のある競合が検出されます (このアルゴリズムの詳細については、「競合検出アルゴリズム」を参照してください)。デッドロック分析では、ロックが取得されるたびにロック サイクルが計算されます。
最後の段階では、報告された全エラーの後処理が行われ、エラーの重大性と危険性を基に優先順位が付けられます。静的分析には誤検知の少なさやバグ検出の信頼性の高さが求められますが、このツールの作者は、こうした要望に応えるために多くの時間を費やしてきました。このツールの検出結果は、すばらしいものです。テスト エンジニアリングの点からすると、非常に実用的であると言えます。
Chord: このツールは、フローではなくコンテキストを識別する、Java 向けの静的分析ツールです。フローを識別しないため、他の静的ツールよりもはるかにスケーラブルですが、その分、正確性が損なわれています。このツールでは、Java で使用できる特殊な同期プリミティブも考慮されています。使用されているアルゴリズムは非常に複雑で、多くの概念が取り入れられています (Chord の詳細については、go.microsoft.com/fwlink/?LinkId=116526 を参照してください)。
KISS: これは、Microsoft Research によって開発された、C 並列プログラム向けのモデル チェッカーです。並列システムでは状態空間がすぐに肥大化するため、KISS では、C 並列プログラムをインタリーブの実行をシミュレートする逐次プログラムに変換します。その後で、逐次モデル チェッカーを使用して分析を行います。
アプリケーションは、並列プログラムを逐次プログラムに変換するステートメントによってインストルメント化されますが、KISS には非確定性を制御する機能があります。非確定的なコンテキストの切り替えは、上の CHESS で説明したのと同様の原理で制限されます。プログラマは、同時実行に関する想定を有効にするために、アサートを含める必要があります。このツールでは誤検知は報告されません。このツールは研究用のプロトタイプであり、主に C コードを使用する Windows ドライバ チームによって使用されてきました (go.microsoft.com/fwlink/?LinkId=115723 を参照してください)。
Zing: このツールは、並列プログラムの設計検証用の純粋なモデル チェッカーです。Zing には複雑な状態と遷移を記述するための独自のカスタム言語があります。また、並列ステート マシンをモデル化する機能を完全に備えています。他のモデル チェッカーと同様に、Zing には設計を検証するための包括的な方法が用意されています。このツールを使用すると、想定の有効性を検証し、特定の状態の有無を形式的に証明できるので、設計品質の信頼性を高めることもできます。また、このツールでは、革新的な抑制手法を利用して同時実行状態空間の肥大化を抑えています。
(プログラムの正確性を確認するために) Zing で使用するモデルは、手動で、またはトランスレータを使用して作成する必要があります。特定の用途に使用するドメイン トランスレータは記述可能ですが、ネイティブ アプリケーションや CLR アプリケーション向けの完全かつ実用的なトランスレータとなると、まだ目にしたことがありません。トランスレータがなければ、プロジェクトの重要なサブセクションの正確性を検証する場合を除き、大規模なソフトウェア プロジェクトで Zing を使用するのは不可能でしょう (go.microsoft.com/fwlink/?LinkId=115725 を参照してください)。

パフォーマンス テスト
パフォーマンス テストは、同時実行のテスト プロセスに欠かせない重要な要素です。結局のところ、並列アプリケーションの主要な開発目的の 1 つは、逐次アプリケーションよりも優れたパフォーマンスを実現することです。アムダールの法則とグスタフソンの法則で仮定されているように、並列アプリケーションで実現されるパフォーマンスの向上は、そのアルゴリズムの並列化可能性の側面、プログラム中の逐次処理が必要な部分の量、並列処理のオーバーヘッド、データ/ワークロードの特性などに大きく左右されます。開発関係者は、パフォーマンス テストを行うことで、並列アプリケーションのパフォーマンス特性を把握し、分析することができます。
並列アプリケーションのパフォーマンス テストを行う際の一般的な手法は、逐次アプリケーションのパフォーマンス テストの手法と同じです。以下のセクションでは、パフォーマンス テストの主要なステップを並列アプリケーションに合わせて実行する方法について説明します。最も有用な測定要素については、「テスト データとレポート メトリックの特定」を参照してください。
パフォーマンス テストにおいて最も重要なステップは、テストの目的と目標を明確にすることです。並列アプリケーションの場合は、使用されているアルゴリズムの並列化可能性とスケーラビリティの理解、設計手段によって異なるパフォーマンス特性の把握、同期と通信のオーバーヘッドの検出、パフォーマンス要件を満たしていることの確認などが、適切なテスト目的と言えます。
パフォーマンス テストの目的と範囲は、テストの実施者によっても変わるという点に注意してください。たとえば、並列アプリケーションをこれから導入する顧客の場合は、ビジネス ニーズを満たしていることを確認するためにパフォーマンス テストを行うことがあります。一方、開発チームの場合は、ボトルネックを特定し、プログラムの並列化可能性を改善するために、徹底的なパフォーマンス テストを行うことに注目している可能性があります。そのうえ、テストの目的はソフトウェアの開発サイクルの間に変化することもあります。テストの目的は、設計フェーズや実装フェーズでは、アプリケーションのパフォーマンス特性を把握し、向上させるためであり、テスト フェーズやリリース フェーズ (安定化フェーズ) では、ビルドによってパフォーマンスが低下しないことを確認するためという具合に変わる可能性があります。
パフォーマンスのテスト プロセスでは、テスト シナリオの作成とその目標の設定も重要になります。並列アプリケーションのパフォーマンス特性を把握したいと考えている開発チームの場合は、顧客シナリオ テスト、主要業績評価指標 (KPI) テスト、ミクロ ベンチマーク (MBM) テストの 3 種類のテスト シナリオを作成する必要があります。
これら 3 種類のパフォーマンス テストの関係は、システム テスト、統合テスト、および単体テストの関係と同等のものと考えることができます。つまり、顧客シナリオ テストは一連の KPI テストにつながり、KPI テストは一連の MBM テストにつながる、と言えます。これらのテスト タイプの関係を理解しておくと、開発者は、並列アプリケーションのさまざまなサブパーツ間のパフォーマンスの関係も理解できます。また、こうした理解があれば、パフォーマンス バグの適切な優先順位付けが可能になります。そして、さらに重要なことですが、結果指向の提案を行うことにより、開発チームはパフォーマンスに関する顧客の苦情に対処できるようになります。
並列アプリケーションの場合は言うまでもありませんが、複雑なアプリケーションのパフォーマンス目標を設定するのは非常に難しい作業です。目標を設定する際の 1 つのアプローチとして、アプリケーションの各コンポーネントとサブコンポーネントに期待するアルゴリズムのパフォーマンス メトリック/理論的パフォーマンス メトリック、比較分析 (比較の候補には競合他社の類似アプリケーションを含む)、および既存の実装/アプリケーション (存在する場合) のプロファイリングから得られる 3 つのメトリックを評価することにより、目標を導き出す方法があります。
テスト目標の場合と同様に、どのテスト シナリオにも一連の判定基準 (テスト結果を検証するための一連の値) を設ける必要があります。並列性のテスト シナリオでは、判定基準として次の要素を使用できます。
テスト結果の変動性 : これは、アプリケーションの不安定性を示します。テスト結果の変動性を明らかにするには、テスト結果の標準偏差を計算します。
CPU 使用率 : CPU 使用率が低い状態は、同時実行に関するバグ (デッドロックや同期オーバーヘッドなど) の兆候である可能性があります。逆に CPU 使用率が高い場合も、必ずしも良好な状態であるとは言えません。ライブロックが発生すると、CPU 使用率が高まります。
ガベージ コレクション : マネージ アプリケーションでは、過剰な数のメモリ管理処理によって実際の処理の実行が妨げられることがあります。その場合は、設計や実装に問題があることがわかります。
スレッドの合計実行時間 : これを基に、プログラムを逐次実行した場合の合計実行時間を見積もることができます。スレッドの合計実行時間を、同じアルゴリズム プログラムを逐次実行した場合の経過時間と比較することで、並列処理のオーバーヘッドを把握できます (また、テストが有効であるかどうかも検証できます)。
メモリの合計使用量 : メモリの合計使用量の測定は、アプリケーションのメモリ プロファイルの把握に役立ちます。マネージ アプリケーションでは、アプリケーションの合計メモリ使用量の変化にガベージ コレクタが大きく関係しています。メモリ使用量を評価する際には、ガベージ コレクタを必ず考慮に入れてください。
パフォーマンス テストを開始する前に 3 つの重要な手順を実行しておくと、より適切な結果を得ることができます。まず、テスト環境の可変性を最小限に抑えます (可変性が生じる原因は、アプリケーションとテスト環境の両方にあります)。テスト環境の可変性を抑えるには、不要なサービスとアプリケーションを停止し、ネットワークの干渉を抑制してください。アプリケーションが原因の可変性については、ウォームアップを繰り返し行い、同じテストを複数回行って測定値を収集することで、その影響を抑えることができます。
次に、マネージ アプリケーションでは、ガベージ コレクタが不適切なタイミングで実行されると、パフォーマンスの結果の有効性が失われることがあります。そこで、パフォーマンスの測定中にガベージ コレクタが呼び出される可能性を最小限に抑える必要があります。そのためには、測定値を収集する前または後にガベージ コレクション (GC) を強制的に実行するか、GCLatencyMode を LowLatency に変更します (Microsoft® .NET Framework 3.5 の場合のみ)。
最後に、メトリックを収集する前に必ずウォームアップを行うようにします。測定値を収集する前にウォームアップとしてテストを実行することで、変数の初期化、(マネージ アプリケーションにおける) JIT 処理のコスト、初期のキャッシュ ミスによる遅延など、1 回限りのコストを抑えることができます。ただし、パフォーマンス シナリオによっては、コールド起動パフォーマンスの測定も重要になる場合があります (アプリケーションの起動パフォーマンス改善の詳細については、msdn2.microsoft.com/magazine/cc337892.aspx を参照してください)。
同時実行テストに関する参考資料

同期を使用する

Bugslayer: Wait Chain Traversal

ハングを解消する : .NET アプリケーションのデッドロックを回避および検出するための高度な手法

ReadWriteBarrier (C++)

Intel 64 アーキテクチャ メモリ オーダリング

同期とマルチプロセッサの問題点

PREfast ステップ バイ ステップ

AppVerifier を使用してアプリケーションをテストする

Web アプリケーション向けパフォーマンス テスト ガイダンス

ストレス テスト
多くの場合、ソフトウェア ストレス テストでは、アプリケーションの堅牢性、可用性、および正常なエラー処理についてテストします。通常、堅牢性をテストするには、同じ機能または同じ機能の組み合わせを繰り返し呼び出して、実行の正確性を確認します。可用性をテストするには、リソースが大量に使用されている状態 (メモリが少ない状態、CPU の負荷が高い状態など) でアプリケーションを実行し、アプリケーションがクラッシュしないことやエラーが適切に発生することを確認します。正常なエラー処理については、多くの場合、エラーを挿入する (フォールト挿入) か、複数のエラーを発生させることでテストします。
並列アプリケーションのストレス テストを行う際には、アプリケーションの堅牢性と可用性を特に重点的にテストする必要があります。並列アプリケーションには非確定的な動作があるため、同じ機能または同じ機能の組み合わせを繰り返し呼び出した際に、異なるコード パスが使用されることがあります。これは、数多くのバグの検出につながる可能性があります。また、リソースが大量に使用されている状態 (非常に多くのスレッドが作成されている状態や、複数のスレッドがデッドロック/ライブロックに陥っている状態) での可用性のテストは特に重要です。並列アプリケーションの場合、ユーザーがそのような場面に遭遇する可能性が比較的高いためです。
ストレスに関係する同時実行のバグの根本原因を分析することや、ストレスに関係するバグを一貫した方法で再現することは、きわめて困難です。とはいえ、同時実行テスト ツールがいまだ発展途上にある現状では、ストレス テストは機能テストを補完する重要な手段です。

まとめ
並列コンピューティングの処理能力を真に活用するためには、逐次アプリケーションを開発、テストするために築かれた現状のソフトウェア開発慣習を進化させる必要があります。並列化によって生じる新種のバグが原因で、並列アプリケーションの開発とテストは難しい作業になっています。この記事では、同時実行に関するさまざまな種類のバグと、そのようなバグを見つけるための戦略の概要を示しました。また、パフォーマンス テストを同時実行のシナリオに利用する方法についても説明しました。

Rahul V. Patil は、マイクロソフトの並列コンピューティング プラットフォーム チームで SDET リードを務め、ネイティブな並列フレームワークを使用したテスト作業を指揮しています。

Boby George もマイクロソフトの並列コンピューティング プラットフォーム チームの SDET であり、マネージ並列コンピューティング フレームワークを使用したパフォーマンス テスト作業を担当しています。

Page view tracker