Oracle Solaris Studio 12.2: スレッドアナライザユーザーズガイド

第 2 章 データの競合チュートリアル

この章は、スレッドアナライザを使用してデータの競合を検出し修正する方法を学ぶ詳細なチュートリアルです。

このチュートリアルは、次の節から構成されています。

2.1 データの競合チュートリアルのソースファイル

このチュートリアルでは、データの競合を含んだ 2 つのプログラムを使用します。

2.1.1 データの競合チュートリアルのソースファイルの入手

このチュートリアルで使用されるソースファイルは、Oracle Solaris システムでは /opt/solstudio12.2/prod/examples/tha に、Linux または OpenSolaris システムでは /opt/oracle/solstudio12.2/prod/examples/tha にあります。例は、 prime_omp および prime_pthr サブディレクトリにあります。例を含んだ各ディレクトリには、手順に関する DEMO ファイルと Makefile ファイルが 1 つずつありますが、このチュートリアルではその手順を無視し、Makefile も使用しません。代わりに、コマンドを個別に実行していきます。

このチュートリアルに沿って学習するには、例を含んだディレクトリから prime_omp.c prime_pthr.c ファイルを、別のディレクトリにコピーするか、自分でファイルを作成し、次のコードリストからコードをコピーしてください。

2.1.2 prime_omp.c のソースコード

prime_omp.c のソースコードは次に示すとおりです。

  1  /*
  2   * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All Rights Reserved.
  3   * @(#)prime_omp.c 1.3 (Oracle) 10/03/26
  4   */
  5
  6  #include <stdio.h>
  7  #include <math.h>
  8  #include <omp.h>
  9
 10  #define THREADS 4
 11  #define N 10000
 12
 13  int primes[N];
 14  int pflag[N];
 15
 16  int is_prime(int v)
 17  {
 18   int i;
 19   int bound = floor(sqrt(v)) + 1;
 20
 21   for (i = 2; i < bound; i++) {
 22          /* no need to check against known composites */
 23          if (!pflag[i])
 24              continue;
 25          if (v % i == 0) {
 26              pflag[v] = 0;
 27              return 0;
 28          }
 29      }
 30      return (v > 1);
 31  }
 32
 33  int main(int argn, char **argv)
 34  {
 35      int i;
 36      int total = 0;
 37
 38  #ifdef _OPENMP
 39      omp_set_dynamic(0);
 40      omp_set_num_threads(THREADS);
 41  #endif
 42
 43      for (i = 0; i < N; i++) {
 44          pflag[i] = 1;
 45      }
 46
 47      #pragma omp parallel for
 48      for (i = 2; i < N; i++) {
 49          if ( is_prime(i) ) {
 50              primes[total] = i;
 51              total++;
 52          }
 53      }
 54
 55      printf("Number of prime numbers between 2 and %d: %d\n",
 56             N, total);
 57
 58      return 0;
 59  }

2.1.3 prime_pthr.c のソースコード

prime_pthr.c のソースコードは次に示すとおりです。

  1  /*
  2   * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All Rights Reserved.
  3   * @(#)prime_pthr.c 1.4 (Oracle) 10/03/26
  4   */
  5
  6  #include <stdio.h>
  7  #include <math.h>
  8  #include <pthread.h>
  9
 10  #define THREADS 4
 11  #define N 10000
 12
 13  int primes[N];
 14  int pflag[N];
 15  int total = 0;
 16
 17  int is_prime(int v)
 18  {
 19      int i;
 20      int bound = floor(sqrt(v)) + 1;
 21
 22      for (i = 2; i < bound; i++) {
 23          /* no need to check against known composites */
 24          if (!pflag[i])
 25              continue;
 26          if (v % i == 0) {
 27              pflag[v] = 0;
 28              return 0;
 29          }
 30      }
 31      return (v > 1);
 32  }
 33
 34  void *work(void *arg)
 35  {
 36      int start;
 37      int end;
 38      int i;
 39
 40      start = (N/THREADS) * (*(int *)arg);
 41      end = start + N/THREADS;
 42      for (i = start; i < end; i++) {
 43          if ( is_prime(i) ) {
 44              primes[total] = i;
 45              total++;
 46          }
 47      }
 48      return NULL;
 49  }
 50
 51  int main(int argn, char **argv)
 52  {
 53      int i;
 54      pthread_t tids[THREADS-1];
 55
 56      for (i = 0; i < N; i++) {
 57          pflag[i] = 1;
 58      }
 59
 60      for (i = 0; i < THREADS-1; i++) {
 61          pthread_create(&tids[i], NULL, work, (void *)&i);
 62      }
 63
 64      i = THREADS-1;
 65      work((void *)&i);
 66
 67      for (i = 0; i < THREADS-1; i++) {
 68          pthread_join(tids[i], NULL);
 69      }
 70
 71      printf("Number of prime numbers between 2 and %d: %d\n",
 72             N, total);
 73
 74      return 0;
 75  }

2.1.3.1 prime_omp.c および prime_pthr.c でのデータの競合の影響

コードに競合状態が含まれ、実行するごとに別々の計算結果が得られる場合、メモリーアクセスの順序は決まっていません。

例をコンパイルして実行できるので、prime_omp または prime_pthr を実行するごとに、コード内のデータの競合によって誤ったまたは矛盾した結果が生じることがわかります。

次の例で、太字のコマンドを入力して、prime_omp プログラムをコンパイルし実行します。


% cc -xopenmp=noopt  -o prime_omp prime_omp.c -lm
%
% ./prime_omp
Number of prime numbers between 2 and 10000: 1229
% ./prime_omp
Number of prime numbers between 2 and 10000: 1228
% ./prime_omp
Number of prime numbers between 2 and 10000: 1229

次の例で、太字のコマンドを入力して、prime_pthr プログラムをコンパイルし実行します。


% cc -mt -o prime_pthr prime_pthr.c -lm
%
% ./prime_pthr
Number of prime numbers between 2 and 10000: 1140
% ./prime_pthr
Number of prime numbers between 2 and 10000: 1122
% ./prime_pthr
Number of prime numbers between 2 and 10000: 1141

各プログラムを 3 回実行した結果が矛盾していることに注意してください。矛盾した結果が表示されるまで、4 回以上プログラムを実行する必要がある場合もあります。

次に、データの競合が生じている位置を特定できるように、コードを計測し、実験を作成します。

2.2 スレッドアナライザを使用したデータの競合の検出方法

スレッドアナライザは、Oracle Solaris Studio パフォーマンスアナライザが使用するものと同じ「収集-分析」モデルに従います。

    スレッドアナライザを使用するには、次の 3 つの手順を行います。

  1. 「2.2.1 コードを計測する」

  2. 「2.2.2 データの競合の検出実験を作成する」

  3. 「2.2.3 データの競合の検出実験を検証する」

2.2.1 コードを計測する

プログラムでデータの競合の検出を可能にするには、実行時にメモリーアクセスを監視するコードをあらかじめ計測しておく必要があります。この計測は、アプリケーションソースコードに行うことも、特定の Oracle コンパイラ最適化フラグでコンパイルされているアプリケーションバイナリに行うこともできます。このチュートリアルでは、プログラムを計測する両方のメソッドの使用方法を示します。

2.2.1.1 ソースコードを計測する

ソースコードを計測するには、特別なコンパイラオプション -xinstrument=datarace を使ってアプリケーションをコンパイルする必要があります。このオプションは、データの競合の検出用に生成したコードを計測するようにコンパイラに指示します。

-xinstrument=datarace コンパイラオプションを、プログラムのコンパイルに使用する既存のオプションセットに追加します。


注 –

-xinstrument=datarace を使ってプログラムをコンパイルするときには、必ず -g オプションも指定してください。アナライザの全機能を有効にするための追加情報が生成されます。データの競合の検出用にプログラムをコンパイルするときには、高度な最適化を指定しないでください。-xopenmp=noopt を使って OpenMP プログラムをコンパイルしてください。高度な最適化を使用した場合、行番号や呼び出しスタックなど、報告された情報が間違っていることがあります。


このチュートリアル用にソースコードを計測するには、次のコマンドを使用できます。


% cc -xinstrument=datarace -g -xopenmp=noopt -o prime_omp_inst prime_omp.c -lm

% cc -xinstrument=datarace -g -o prime_pthr_inst prime_pthr.c -lm

例では、バイナリが計測済みバイナリであることがわかるように、出力ファイルの末尾に _inst と指定されていることに注意してください。これは必須ではありません。

2.2.1.2 バイナリコードを計測する

ソースコードの代わりにプログラムのバイナリコードを計測するには、discover ツールを使用する必要があります。このツールは、Oracle Solaris Studio に含まれ、discover(1) のマニュアルページと『Oracle Solaris Studio 12.2 Discover および Uncover ユーザーズガイド』で説明されています。

バイナリ計測の要件については、「バイナリレベルの計測」を参照してください。

チュートリアルの例では、次のコマンドを入力して、最適化レベル 3 でコードをコンパイルし、discover で使用できるバイナリを作成します。


% cc -xopenmp=noopt -g -o prime_omp_opt prime_omp.c -lm

% cc -g -O3 -o prime_pthr_opt prime_pthr.c -lm

続いて、discover を、作成した prime_omp_opt および prime_pthr_opt 最適化済みバイナリで実行します。


% discover -i datarace -o prime_omp_disc prime_omp_opt

% discover -i datarace -o prime_pthr_disc prime_pthr_opt

これらのコマンドは計測済みバイナリ、prime_omp_disc および prime_pthr_disc を作成します。これらのバイナリを collect で使用して、スレッドアナライザで検証する実験を作成できます。

2.2.2 データの競合の検出実験を作成する

-r race フラグを付けて collect コマンドを使用してプログラムを実行し、プロセスの実行中にデータの競合の検出実験を作成します。OpenMP プログラムの場合、使用されるスレッド数が 1 より大きいことを確認してください。チュートリアルの例では 4 つのスレッドが使用されます。

ソースコードを計測して作成したバイナリから実験を作成するには、次のスレッドを使用します。


% collect -r race -o prime_omp_inst.er prime_omp_inst

% collect -r race -o prime_pthr_inst.er prime_pthr_inst

discover ツールを使用して作成したバイナリから実験を作成するには、次のスレッドを使用します。


% collect -r race -o prime_omp_disc.er prime_omp_disc

% collect -r race -o prime_pthr_disc.er prime_pthr_disc

データの競合を検出する可能性を高めるには、-r race フラグ付きで collect を使用して、複数のデータの競合の検出実験を作成することをお勧めします。実験ごとに異なるスレッド数と異なる入力データを使用してください。

たとえば prime_omp.c では、スレッド数は次の行で設定されます。


#define THREADS 4

この 4 を 1 より大きな他の整数 (たとえば 8) に変えると、スレッド数を変更できます。

prime_omp.c の次の行は、2 ~ 3000 の素数を検出するようにプログラムを制限します。


#define N 3000

N の値を変更して別の入力データを指定すると、プログラム作業量を増減できます。

2.2.3 データの競合の検出実験を検証する

スレッドアナライザ、パフォーマンスアナライザ、er_print ユーティリティで、データの競合の検出実験を検証できます。スレッドアナライザおよびパフォーマンスアナライザはどちらも GUI インタフェースを表示します。スレッドアナライザはデフォルトの簡略セットのタブを表示しますが、それ以外はパフォーマンスアナライザと同じです。

2.2.3.1 スレッドアナライザを使用したデータの競合実験の表示

スレッドアナライザを開始するには、次のコマンドを入力します。


% tha

スレッドアナライザ GUI は、メニューバー、ツールバー、および各種表示用のタブを含む分割区画で構成されます。

左側の区画には、デフォルトで次の 3 つのタブが表示されます。

スレッドアナライザ画面の右側区画には、次の 2 つのタブが表示されます。

2.2.3.2 er_print を使用したデータの競合実験の表示

er_print ユーティリティは、コマンド行インタフェースを表示します。インタラクティブセッションで er_print ユーティリティを使用して、セッション中にサブコマンドを指定します。コマンド行オプションを使用して、インタラクティブでない方法でもサブコマンドを指定できます。

次のサブコマンドは、er_print ユーティリティで競合を調べるときに役立ちます。

詳細は、collect(1)、tha(1)、analyzer(1)、および er_print(1) のマニュアルページを参照してください。

2.3 実験結果について

この節では、er_print コマンド行とスレッドアナライザ GUI の両方を使用して、検出したデータの競合それぞれに関する次の情報を表示する方法について説明します。

2.3.1 prime_omp.c でのデータの競合

prime_omp.c でのデータの競合を調べるには、「2.2.2 データの競合の検出実験を作成する」で作成したいずれかの実験を使用できます。

er_printprime_omp_instr.er 実験のデータの競合情報を表示するには、次のコマンドを入力します。


% er_print prime_omp_inst.er

(er_print) プロンプトで races と入力すると、次のような出力が表示されます。


(er_print)races

Total Races:  2 Experiment:  prime_omp_inst.er

Race #1, Vaddr: 0x21ca8
      Access 1: Write, is_prime,
                       line 26 in "prime_omp.c"
      Access 2: Read,  is_prime,
                       line 23 in "prime_omp.c"
  Total Callstack Traces: 1

Race #2, Vaddr: (Multiple Addresses)
      Access 1: Write, main,
                       line 50 in "prime_omp.c"
      Access 2: Write, main,
                       line 50 in "prime_omp.c"
  Total Callstack Traces: 2
(er_print)

この特定のプログラム実行中に、2 つのデータの競合が生じました。

スレッドアナライザで prime_omp_inst.er 実験結果を開くには、次のコマンドを入力します。


% tha prime_omp_inst.er

次のスクリーンショットには、スレッドアナライザに表示された、prime_omp.c で検出された競合が示されています。

図 2–1 prime_omp.c で検出されたデータの競合

prime_omp.c の「競合 (Races)」タブを表示した「スレッドアナライザ (Thread Analyzer)」ウィンドウのスクリーンショット。

prime_omp.c には、次の 2 つのデータの競合が示されています。

スレッドアナライザの「デュアルソース (Dual Source)」タブでは、データの競合に関連付けられた 2 つのソース位置を同時に確認できます。たとえば、「競合 (Races)」タブで prime_pthr.cRace #2 を選択し、続いて「デュアルソース (Dual Source)」タブをクリックします。次のように表示されます。

図 2–2 prime_omp.c で検出されたデータの競合のソースコード

prime_omp.c でのデータの競合について「デュアルソース (Dual Sources)」タブを示した、「スレッドアナライザ (Thread Analyzer)」ウィンドウのスクリーンショット。


ヒント –

「デュアルソース (Dual Source)」タブの左マージンに「競合アクセス (Race Accesses)」メトリックを表示するには、各ソースパネルのヘッダー上にマウスをドラッグする必要があります。


2.3.2 prime_pthr.c でのデータの競合

prime_pthr.c でのデータの競合を調べるには、「2.2.2 データの競合の検出実験を作成する」で作成したいずれかの実験を使用できます。

er_printprime_pthr_instr.er 実験のデータの競合情報を表示するには、次のコマンドを入力します。


% er_print prime_pthr_inst.er

(er_print) プロンプトで races と入力すると、次のような出力が表示されます。


(er_print) races

Total Races:  4 Experiment:  prime_pthr_inst.er

Race #1, Vaddr: (Multiple Addresses)
      Access 1: Write, is_prime + 0x00000270,
                       line 27 in "prime_pthr.c"
      Access 2: Write, is_prime + 0x00000270,
                       line 27 in "prime_pthr.c"
  Total Callstack Traces: 2

Race #2, Vaddr: 0xffbfe714
      Access 1: Write, main + 0x0000025C,
                       line 60 in "prime_pthr.c"
      Access 2: Read,  work + 0x00000070,
                       line 40 in "prime_pthr.c"
  Total Callstack Traces: 1

Race #3, Vaddr: (Multiple Addresses)
      Access 1: Write, work + 0x00000150,
                       line 44 in "prime_pthr.c"
      Access 2: Write, work + 0x00000150,
                       line 44 in "prime_pthr.c"
  Total Callstack Traces: 2

Race #4, Vaddr: 0x21a90
      Access 1: Write, work + 0x00000198,
                       line 45 in "prime_pthr.c"
      Access 2: Write, work + 0x00000198,
                       line 45 in "prime_pthr.c"
  Total Callstack Traces: 2
(er_print)

この特定のプログラム実行中に、4 つのデータの競合が生じました。

スレッドアナライザで prime_pthr_inst.er 実験結果を開くには、次のコマンドを入力します。


% tha prime_pthr_inst.er

次のスクリーンショットには、スレッドアナライザに表示された、prime_pthr.c で検出された競合が示されています。er_print で示された競合と同じであることに注意してください。

図 2–3 prime_pthr.c で検出されたデータの競合

prime_pthr.c の「競合 (Races)」タブを表示した「スレッドアナライザ (Thread Analyzer)」ウィンドウのスクリーンショット

prime_pthr.c には、次の 4 つのデータの競合が示されています。

Race #2 を選択した後に「デュアルソース (Dual Source)」タブをクリックした場合、次のスクリーンショットのように、2 つのソース位置が表示されます。

図 2–4 データの競合のソースコード詳細

「デュアルソース (Dual Source)」タブを示した「スレッドアナライザ (Thread Analyzer)」ウィンドウのスクリーンショット。

Race #2 の最初のアクセスは行 60 で行われ、上部のパネルに表示されます。2 番目のアクセスは行 40 で行われ、下部のパネルに表示されます。ソースコードの左側に「競合アクセス (Race Accesses)」メトリックが強調表示されます。このメトリックは、その行でデータの競合アクセスが報告された回数を示します。

2.3.3 データの競合の呼び出しスタックトレース

スレッドアナライザの「競合 (Race)」タブで一覧表示されたデータの競合ごとに、1 つまたは複数の呼び出しスタックトレースが関連付けられています。呼び出しスタックは、データの競合を招く、コード内の実行パスを表示します。「呼び出しスタックトレース (Call Stack Trace)」をクリックすると、右側パネルの「競合の詳細 (Race Details)」タブに、データの競合を招く関数呼び出しが表示されます。

図 2–5 prime_omp.c の呼び出しスタックトレースを示した「競合 (Races)」タブ

prime_omp.cでのデータの競合の呼び出しスタックトレースを示した「競合の詳細 (Race Details)」タブを表示した「スレッドアナライザ (Thread Analyzer)」ウィンドウのスクリーンショット

2.4 データの競合の原因の診断

この節では、データの競合の原因を診断する基本的な方法について説明します。

2.4.1 データの競合が誤検知であるかどうかをチェックする

誤検知のデータの競合は、スレッドアナライザで報告されますが、実際には起こっていないデータの競合です。スレッドアナライザは、報告する誤検知の数を減らそうと試みます。ただし、ツールが正確なジョブを行えずに、誤検知のデータの競合を報告する場合があります。

誤検知のデータの競合は本当のデータの競合ではなく、したがってプログラムの動作に影響しないので、このデータの競合は無視できます。

誤検知のデータの競合の例については、「2.5 誤検知」を参照してください。レポートから誤検知のデータの競合を削除する方法については、「A.1 スレッドアナライザユーザー API」を参照してください。

2.4.2 データの競合が影響のないものであるかどうかを確認する

影響のないデータの競合は、存在していてもプログラムの正確さには影響しない意図的なデータの競合です。

一部のマルチスレッドアプリケーションでは、データの競合を引き起こすコードを意図的に使用します。設計によってデータの競合が存在するので、修正は必要ありません。ただし、場合によっては、このようなコードを正しく実行させるには非常に慎重を要します。これらのデータの競合については注意深く調べてください。

影響のない競合については、「2.5 誤検知」を参照してください。

2.4.3 データの競合ではなくバグを修正する

スレッドアナライザは、プログラム内でデータの競合を見つけるときに役立ちますが、プログラム内のバグを自動的に見つけることも、見つかったデータの競合の修正方法を提示することもできません。データの競合は、バグによって生じることもあります。バグを見つけて修正することが重要です。単にデータの競合を取り除くだけでは正しいアプローチにはならず、以降のデバッグがさらに困難になる可能性があります。

2.4.3.1 prime_omp.c でのバグの修正

ここでは、prime_omp.c でのバグを修正する方法について説明します。完全なファイルのリストについては、「「2.1.2 prime_omp.c のソースコード」」を参照してください。

配列 primes[ ] の要素でのデータの競合を削除するために、行 50 および 51 を critical セクションに移します。

47      #pragma omp parallel for
48      for (i = 2; i < N; i++) {
49          if ( is_prime(i) ) {
                 #pragma omp critical

                 {
50                  primes[total] = i;
51                  total++;
                 }
52          }
53     }

また、次のように行 50 および 51 を 2 つの critical セクションに移すこともできますが、この変更ではプログラムを修正できません。

47      #pragma omp parallel for
48      for (i = 2; i < N; i++) {
49          if ( is_prime(i) ) {
                 #pragma omp critical
                 {
50                  primes[total] = i;
                 }
                 #pragma omp critical
                 {
51                  total++;
                 }
52          }
53     }

スレッドは、排他的ロックを使用して primes[ ] 配列へのアクセスを制御しているので、行 50 および 51 の critical セクションによってデータの競合が取り除かれます。ただし、プログラムはまだ正しくありません。2 つのスレッドは、同じ合計値を使用して primes[ ] の同じ要素を更新する可能性があり、primes[ ] の要素の中には、値がまったく割り当てられないものが生じる可能性があります。

行 23 での pflag[ ] からの読み取りと、行 26 での pflag[ ] への書き込みとの 2 番目のデータの競合は間違った結果を招かないので、実際には影響のない競合です。影響のないデータの競合の修正は必須ではありません。

2.4.3.2 prime_pthr.c でのバグの修正

ここでは、prime_pthr.c でのバグを修正する方法について説明します。完全なファイルのリストについては、「2.1.3 prime_pthr.c のソースコード」を参照してください。

単一の相互排他ロックを使用して、行 44 での prime[ ] のデータの競合と、行 45 での total のデータの競合を取り除きます。

行 60 での i への書き込みと、行 40 での (*arg という) 同じメモリー位置からの読み取りとのデータの競合と、行 27 での pflag[ ] のデータの競合は、別々のスレッドによる変数 i への共有アクセスに問題があることを明らかにします。prime_pthr.c の初期スレッドは、行 60 ~ 62 でループの子スレッドを作成し、関数 work() で動作するようにこれらをディスパッチします。ループ インデックス i は、アドレスで work() に渡されます。すべてのスレッドは i に対して同じメモリー位置にアクセスするので、各スレッドの i の値は一意のままではありませんが、初期スレッドがループ インデックスを増やすたびに変化します。別々のスレッドが同じ i の値を使用するので、データの競合が起こります。問題を修正する 1 つの方法は、i をアドレスではなく値で work() に渡すことです。

次に、修正されたバージョンの prime_pthr.c を示します。

  1  /*
  2   * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All Rights Reserved.
  3   * @(#)prime_pthr_fixed.c 1.3 (Oracle) 10/03/26
  4   */
  5
  6  #include <stdio.h>
  7  #include <math.h>
  8  #include <pthread.h>
  9
 10  #define THREADS 4
 11  #define N 10000
 12
 13  int primes[N];
 14  int pflag[N];
 15  int total = 0;
 16  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 17
 18  int is_prime(int v)
 19  {
 20      int i;
 21      int bound = floor(sqrt(v)) + 1;
 22
 23      for (i = 2; i < bound; i++) {
 24          /* no need to check against known composites */
 25          if (!pflag[i])
 26              continue;
 27          if (v % i == 0) {
 28              pflag[v] = 0;
 29              return 0;
 30          }
 31      }
 32      return (v > 1);
 33  }
 34
 35  void *work(void *arg)
 36  {
 37      int start;
 38      int end;
 39      int i;
 40
 41      start = (N/THREADS) * ((int)arg) ;
 42      end = start + N/THREADS;
 43      for (i = start; i < end; i++) {
 44          if ( is_prime(i) ) {
 45              pthread_mutex_lock(&mutex);
 46              primes[total] = i;
 47              total++;
 48              pthread_mutex_unlock(&mutex);
 49          }
 50      }
 51      return NULL;
 52  }
 53
 54  int main(int argn, char **argv)
 55  {
 56      int i;
 57      pthread_t tids[THREADS-1];
 58
 59      for (i = 0; i < N; i++) {
 60          pflag[i] = 1;
 61      }
 62
 63      for (i = 0; i < THREADS-1; i++) {
 64          pthread_create(&tids[i], NULL, work, (void *)i);
 65      }
 66
 67      i = THREADS-1;
 68      work((void *)i);
 69
 70      for (i = 0; i < THREADS-1; i++) {
 71          pthread_join(tids[i], NULL);
 72      }
 73
 74      printf("Number of prime numbers between 2 and %d: %d\n",
 75             N, total);
 76
 77      return 0;
 78  }

2.5 誤検知

スレッドアナライザは、実際にはプログラム内で生じていないデータの競合を報告する場合があります。これらは誤検知と呼ばれます。ほとんどの場合、誤検知は、ユーザー定義の同期によって、またはさまざまなスレッドでリサイクルされるメモリーによって引き起こされます。詳しくは、「2.5.1 ユーザー定義の同期」および「2.5.2 さまざまなスレッドでリサイクルされるメモリー」を参照してください。

2.5.1 ユーザー定義の同期

スレッドアナライザは、OpenMP、POSIX スレッド、および Solaris スレッドで提供されるほとんどの標準同期 API と構文を認識できます。ただし、ツールはユーザー定義の同期を認識できず、このような同期がコードに含まれる場合、誤検知のデータの競合を報告することがあります。


注 –

このような誤検知のデータの競合を報告しないようにするために、スレッドアナライザには、ユーザー定義の同期が実行されたときにツールに通知するために使用できる一連の API が用意されています。詳しくは、「A.1 スレッドアナライザユーザー API」 を参照してください。


なぜ API を使用する必要があるかを説明するため、次のように考えてみましょう。スレッドアナライザは、CAS 命令を使用したロックの実装、ビジー待機を使用した送信および待機操作などを認識できません。次に、プログラムが POSIX スレッド状態変数の一般的な使用法を採用した、誤検知のクラスの一般的な例を示します。

/* Initially ready_flag is 0 */
 
/* Thread 1: Producer */
100   data = ...
101   pthread_mutex_lock (&mutex);  
102   ready_flag = 1;
103   pthread_cond_signal (&cond);
104   pthread_mutex_unlock (&mutex);
...
/* Thread 2: Consumer */
200   pthread_mutex_lock (&mutex);
201   while (!ready_flag) {
202       pthread_cond_wait (&cond, &mutex);   
203   }
204   pthread_mutex_unlock (&mutex);
205   ... = data;

pthread_cond_wait() 呼び出しは、通常、プログラムエラーと疑わしいウェイクアップから保護するために述語をテストするループ内で行われます。述語のテストおよび設定は、多くの場合、相互排他ロックによって保護されます。前述のコードでは、スレッド 1 は行 100 で変数 data の値を生成し、行 102 で ready_flag の値を 1 に設定してデータが生成されていることを示します。続いて、 pthread_cond_signal() を呼び出して、消費者スレッドであるスレッド 2 を呼び起こします。スレッド 2 はループ内の述語 (!ready_flag) をテストします。フラグが設定されていることを検出すると、行 205 でデータを消費します。

行 102 での ready_flag の書き込みと行 201 での ready_flag の読み取りは、同じ相互排他ロックで保護されています。したがって、2 つのアクセス間にデータの競合はなく、ツールは正しく認識します。

行 100 での data の書き込みと、行 205 での data の読み取りは、相互排他ロックによって保護されません。ただし、プログラムロジックでは、フラグ変数 ready_flag のために行 205 での読み取りは常に、行 100 での書き込み後に行われます。この結果、データへのこれら 2 つのアクセス間にデータの競合は生じません。ただし、pthread_cond_wait() の呼び出し (行 202) が実際には実行時に呼び出されない場合、ツールは、2 つのアクセス間でデータの競合があると報告します。行 201 が実行される前に行 102 が実行された場合は、行 201 が実行されると、ループエントリテストは失敗し、行 202 はスキップされます。ツールは pthread_cond_signal() 呼び出しおよび pthread_cond_wait() 呼び出しを監視し、それらを組み合わせて同期を派生できます。行 202 で pthread_cond_wait() が呼び出されない場合、行 100 での書き込みが常に行 205 の読み取り前に実行されることがツールにはわかりません。したがって、これらが同時に実行されていると見なし、これらの間でデータの競合が生じていると報告します。

libtha(3C) のマニュアルページと「A.1 スレッドアナライザユーザー API」 では、API を使用して、このような誤検知のデータの競合を報告しないようにする方法について説明しています。

2.5.2 さまざまなスレッドでリサイクルされるメモリー

一部のメモリー管理ルーチンは、あるスレッドが別のスレッドで使用できるように解放したメモリーをリサイクルします。スレッドアナライザは、別々のスレッドが使用する同じメモリー位置の寿命が重複していないことを認識できない場合があります。これが起きたときに、ツールは誤検知のデータの競合を報告することがあります。次の例は、この種の誤検知を示しています。

/*----------*/                    /*----------*/
/* Thread 1 */                    /* Thread 2 */
/*----------*/                    /*----------*/
 ptr1 = mymalloc(sizeof(data_t));
 ptr1->data = ...
 ...
 myfree(ptr1);

                                  ptr2 = mymalloc(sizeof(data_t));
                                  ptr2->data = ...
                                  ...
                                  myfree(ptr2);

スレッド 1 とスレッド 2 は同時に実行します。各スレッドは、プライベートメモリーに使用されるメモリーのチャンクを割り当てます。ルーチン mymalloc() は、myfree() の以前の呼び出しによって解放されたメモリーを提供できます。スレッド 2 は、スレッド 1 が myfree() を呼び出す前に、mymalloc() を呼び出します。この場合、ptr1ptr2 は別々の値を取り、2 つのスレッド間でデータの競合は生じません。ただし、スレッド 1 が myfree () を呼び出した後にスレッド 2 が mymalloc() を呼び出した場合、ptr1ptr2 が同じ値を取ることがあります。スレッド 1 はこのメモリーにアクセスできなくなるので、データの競合は生じません。ただし、mymalloc() がメモリーをリサイクルしていることがわかっていない場合、ツールは、ptr1 データの書き込みと ptr2 データの書き込みとのデータの競合を報告します。この種の誤検知は、多くの場合、C++ アプリケーションで、C++ 実行時ライブラリがメモリーを一時変数用にリサイクルするときに起こりますまたしばしば、独自のメモリー管理ルーチンを実装したユーザーアプリケーションでも起こります。現在、スレッドアナライザは、標準の malloc()calloc()、および realloc() インタフェースで実行されたメモリー割り当ておよび解放操作を認識できます。

2.6 影響のないデータの競合

マルチスレッドアプリケーションの中には、パフォーマンスを高めるためにデータの競合を意図的に許可する場合があります。影響のないデータの競合は、存在していてもプログラムの正確さには影響しない意図的なデータの競合です。次の例は、影響のないデータの競合を示します。


注 –

影響のないデータの競合以外でも、大きなクラスのアプリケーションでは、正しく設計するのが困難なロックフリーおよびウェイトフリーアルゴリズムを使用しているので、データの競合を許可します。スレッドアナライザは、これらのアプリケーションでのデータの競合の位置を特定する場合に役立ちます。


2.6.1 素数検索用のプログラム

prime_omp.c 内のスレッドは、関数 is_prime() を実行することによって、整数が素数かどうかをチェックします。

    16  int is_prime(int v)
    17  {
    18      int i;
    19      int bound = floor(sqrt(v)) + 1;
    20
    21      for (i = 2; i < bound; i++) {
    22          /* no need to check against known composites */
    23          if (!pflag[i])
    24              continue;
    25          if (v % i == 0) {
    26              pflag[v] = 0;
    27              return 0;
    28          }
    29      }
    30      return (v > 1);
    31  }

スレッドアナライザは、行 26 での pflag[ ] への書き込みと行 23 での pflag[ ] の読み取りとの間にデータの競合があることを報告します。ただし、このデータの競合は、最終的な結果の正確さには影響しないので影響のないものです。23 行で、スレッドは、所与の i の値で、pflag[i] が 0 に等しいかどうかをチェックします。pflag[i] が 0 に等しい場合、i が既知の合成数である (つまり、 i は素数でないとわかっている) ことを意味します。この結果、vi で割り切れるかどうかをチェックする必要がなくなります。v がいずれかの素数で割り切れるかどうかだけをチェックすればよくなります。したがって、pflag[i] が 0 に等しい場合、スレッドは i の次の値に進みます。pflag[i] が 0 に等しくなく、vi で割り切れる場合、スレッドは 0 を pflag[v] に割り当てて、v が素数でないことを示します。

正確さの観点からは、複数のスレッドが同じ pflag[ ] 要素をチェックし、同時にそれに書き込むかどうかは重要ではありません。pflag[ ] 要素の初期値は 1 です。スレッドはこの要素を更新するときに、0 の値を割り当てます。つまり、スレッドはその要素に対してメモリーの同じバイトの同じビットに 0 を格納します。現在のアーキテクチャーでは、このような格納は不可分であると想定することが安全です。つまり、その要素がスレッドによって読み取られるときに、読み取られる値は 1 か 0 のどちらかになります。スレッドは、0 の値を割り当てる前に所定の pflag[ ] 要素をチェックする場合 (行 23)、行 25 ~ 28 を実行します。その間に別のスレッドがその同じ pflag[ ] 要素に 0 を割り当てた場合も (行 26)、最終結果は変化しません。これは、基本的に、最初のスレッドが不必要に行 25 ~ 28 を実行したが、最終結果は同じであったことを意味します。

2.6.2 配列値の型を検証するプログラム

スレッドのグループが check_bad_array() を同時に呼び出して、配列 data_array の要素が「間違っている」かどうかをチェックします。各スレッドは配列の異なるセクションをチェックします。スレッドは、要素が間違っていることを検出した場合、グローバル共有変数 is_bad の値を true に設定します。

20  volatile int is_bad = 0;
 ...

 100  /* 
 101   * Each thread checks its assigned portion of data_array, and sets 
 102   * the global flag is_bad to 1 once it finds a bad data element.
 103   */
 104  void check_bad_array(volatile data_t *data_array, unsigned int thread_id)    
 105  {
 106     int i;
 107     for (i=my_start(thread_id); i<my_end(thread_id); i++) {
 108          if (is_bad) 
 109              return;
 110          else {
 111              if (is_bad_element(data_array[i])) { 
 112                  is_bad = 1;
 113                  return;
 114              }
 115          }
 116     }
 117  }

行 108 での is_bad の読み取りと行 112 での is_bad への書き込みとの間にデータの競合があります。ただし、データの競合は最終結果の正確さに影響しません。

is_bad の初期値は 0 です。スレッドは is_bad を更新するときに、値 1 を割り当てます。つまり、スレッドは、is_bad に対してメモリーの同じバイトの同じビットに 1 を格納します。現在のアーキテクチャーでは、このような格納は不可分であると想定することが安全です。したがって、is_bad がスレッドで読み取られるときに、読み取られる値は 0 か 1 のどちらかになります。スレッドは、値 1 が割り当てられる前に is_bad をチェックする場合 (行 108)、for ループの実行を継続します。その間に別のスレッドが値 1 を is_bad に割り当てても (行 112)、最終結果は変化しません。スレッドが for ループを必要以上長時間実行したというだけのことです。

2.6.3 二重検査されたロックを使用したプログラム

シングルトンは、特定の種類のオブジェクトが、プログラム全体で 1 つしか存在しないようにします。二重検査されたロックは、マルチスレッドアプリケーションでシングルトンを初期化する一般的で効率的な方法です。次のコードは、この実装方法を示します。

 100 class Singleton {
 101     public:
 102     static Singleton* instance();
 103     ...
 104     private:
 105     static Singleton* ptr_instance;
 106 };
 ...

 200 Singleton* Singleton::ptr_instance = 0;
 ...

 300 Singleton* Singleton::instance() {
 301     Singleton *tmp = ptr_instance;
 302     memory_barrier();
 303     if (tmp == NULL) {
 304         Lock();
 305         if (ptr_instance == NULL) {
 306             tmp = new Singleton;
 307             memory_barrier();
 308             ptr_instance = tmp;
 309         }
 310         Unlock();
 311     }
 312     return tmp;
 313 }

ptr_instance の読み取り (行 301) は、ロックによって意図的に保護されていません。このため、マルチスレッド環境でシングルトンがすでにインスタンス化されているかどうかを判別するチェックが効率的になります。変数 ptr_instance の行 301 での読み取りと行 308 での書き込みとの間でデータの競合があるが、プログラムは正しく動作することに注意してください。ただし、データの競合を許可する正しいプログラムを作成すると、余分な注意が必要になります。たとえば、前述の二重検査されたロックコードでは、シングルトンおよび ptr_instance を適切な順序で設定および読み取りできるように、行 302 および 307 での memory_barrier() の呼び出しが使用されます。この結果、すべてのスレッドは連続してこれらを読み取ります。このプログラムテクニックは、メモリーバリアーを使用しない場合には機能しません。