SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

インテルTBBを通じて学ぶ並列処理

マルチスレッドを安全に実行する

スレッドセーフの勘所

  • X ポスト
  • このエントリーをはてなブックマークに追加

「競合問題」の解決

 「1つのインスタンスが複数のスレッドで共有される」ことで問題が発生するなら、スレッドごとにインスタンスを分ければ、問題は解決します。最初からマルチスレッド化することがわかっていて、設計から行うならば、設計の段階でマルチスレッド化する箇所を洗い出し、スレッド間で共通して使用するインスタンスがないように設計を行います。

 しかし、今回取り上げたような、オブジェクトを保存しておくためのアルゴリズム、「コンテナ」では、データを蓄えている変数や、どこまでデータを蓄えているかを示すインデックスなどのインスタンスを、複数のスレッドで共有しなければ意味をなしません。そこで、共有しながら解決できる方法を探します。

 では、もう一度問題に戻ります。先ほど「複数のスレッドで、インスタンスを共有することが問題」と説明しました。「インスタンスを共有すること」が、なぜ問題になるのでしょうか。共有しなければならないなら、共有することで起こる問題を解決する方法を探そう、ということです。すなわち「共有しても問題のないアルゴリズム」が、解決方法となるわけです。

 前ページで記載した表1を見ます。スレッド2がindexの範囲を確認した後に、スレッド1でindexの値を変更しています。こうすると、せっかく範囲を確認したことが無駄になってしまいます。つまりここは、indexの範囲を確認してからインクリメントするまでの間、他のスレッドがindexの値を参照してはいけないのです。そこで、この範囲を「複数のスレッドで同時に実行すること」を禁止します。

 禁止する方法は、処理系によって用意されています。ここでは「lock (オブジェクト) 処理」とすることで、同じ「オブジェクト」を参照するスレッドからは「処理」が実行できない、という実装であると仮定し、List1を修正します。Windowsでは、ミューテックスを使ってロックを行う場合が多いです。OpenMPでは、クリティカルディレクティブによって指定します。使用している処理系による禁止の方法を調べて使用してください。

List2.マルチスレッドでの競合問題を修正した仮想コード
#define ARRAY_MAX 1000
static char array[ARRAY_MAX];
static int index = 0;
static int lockObject = 0;

int push(char v) {
    lock (&lockObject) {
        if (index < ARRAY_MAX) {  // 参照して、
            array[index] = v;
            ++index;  // 変更する
        }
    }
    return index;
}

char pop(void) {
    lock (&lockObject) {
        if (index > 0) {  // 参照して、
            return array[index];
            --index;  // 変更する
        }
    }
    return NULL;
}

int howmany(void) {
    lock (&lockObject) {
        return index;
    }
}
表2
実行前の スレッド1 スレッド2 実行後の
index値 実行行 実行行 index値
999 int push(char v) {   999
999 lock (&lockObject) { int push(char v) { 999
999 if (index < ARRAY_MAX) { lock (&lockObject) { 999
999 array[index] = v; ロック待ち 999
999 ++index = v; ロック待ち 1000
1000 } // lock 終了 if (index < ARRAY_MAX) { 1000
1000 return index; } // lock 終了 1000
1000   return index; 1000

「1行」なら安全か

(コメント インドリ (2010/03/03 09:12)より)

>スケジューラーの初期化について

並列処理では実行順序が保障されません。しかも、アセンブラレベルで命令が実行されております。その状態なのですから、初心者がTBBとネイティブスレッド・OpenMPなどの処理を併用させたときに、どのようなプログラムでも正しく実行できると100%の自信を持って言えるでしょうか?私はそんな危険を冒すよりも1行のプログラムを書く方を勧めます。

(コメント インドリ (2010/03/03 15:55)より)

ですから普通の人は・・・

cout << "foo" << .....

(なんらかの処理をする)

cout << "bar" << ...

とプログラミングするでしょう。

また必ず一行で書くという行為はcoutという標準出力オブジェクトの不変項と事後条件を満たしていると言えるでしょうか?

 本文中に、「coutはスレッドセーフではない」と書かれていることについてです。ここに引用したように、「一行で書けば安全だ」と理解できる論調なので、これについて間違っていることを解説します。

 まず、「coutはスレッドクリティカルか」という問題についてです。私が調べた範囲、Microsoft、SUNによる実装は、スレッドセーフであるとリファレンスに記載されています。GNUについては、POSIXがスレッドセーフであることを要求するので、準拠したライブラリを使用するならばスレッドセーフであると、記載されています。参考資料に挙げておきます。よほどユニークなライブラリでない限り、「スレッドセーフである」と言って大丈夫でしょう。ただし、注意してください。C++の標準では、スレッドセーフについて規定されていません。お使いの環境で、ドキュメントを確認してから使用してください。

 次に、「一行で書く」ということについてです。本記事では、「変数を参照する」「変数を更新する」という“2行”に渡る処理を取り上げました。これが“1行”だったら問題が発生しないかと言うと、そうではありません。インドリ氏もコメントでは触れられていますが、人が書く1行のコードは、複数のアセンブリコードに翻訳(コンパイル)されます。アセンブリコードが複数の命令になる場合、問題が発生する可能性があります。アセンブリコードが想像できなくても大丈夫です。for文を思い浮かべてください。標準的なfor文は、1行の中に3つの実行文があります。同じように、List3のように書けば、複数行でありながら1実行文となります。ソースコードの行数と実行される命令数、アセンブリレベルでの命令数には、何の関係もありませんし、関係を持たせるべきではありません。

List3.1命令文を複数行に書く
int
c
=
1;

 最後に、もっとも重要なことです。インドリ氏の一連の発言では「関数がスレッドセーフであること」と、「アプリケーションがスレッドセーフであること」を混同しています。この混同のために、コメント投稿者との会話に齟齬が発生しています。インドリ氏以外は、この2つを混同していません。この2つは同じではなく、分けて考えなければなりません。例えば、インドリ氏が「並列処理で使用して安全である」として取り上げている「concurrent_bounded_queue」であっても、複数のスレッドから同時にpushした場合、どの順番でコンテナに挿入されるかは分かりません。これは、coutに対して「並列処理時にcoutに出力を指定するとコンソール画面の表示が滅茶苦茶になります」と書いてあるのと同じことなので、concurrent_bounded_queueも「スレッドセーフではない」事になってしまいます。インドリ氏の記事では、他の記事を見ても、1命令を多数のスレッドで同時に呼び出すということをしていないため、この問題に気がつかなかったと思われます。

 繰り返しますが、スレッドセーフな命令だけで作ったからといって、アプリケーションが「スレッドセーフ」、この場合は設計意図通りに実行結果が現れるわけではありません。アプリケーションがスレッドセーフになるためには、開発者がスレッドクリティカルな部分について適切に処理を組む必要があります。

 上の引用について、私のブログの方で「「1行のプログラムを書く」というのは、TBBの初期化を指しているのではないか。」というご指摘をいただきました。なるほど、「>スケジューラーの初期化について」とされているので、「task_scheduler_init init;」の一行を挿入することと理解する方が正しい様です。ただ、私がここに「挿入する」と書いたように、「1行のプログラムを書く」ではなく、「1行挿入すること」と書いてあれば、より誤解が少なかったでしょう。

次のページ
まとめ

修正履歴

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
インテルTBBを通じて学ぶ並列処理連載記事一覧

もっと読む

この記事の著者

はなおかじった(ハナオカジッタ)

わんくま同盟で、ブログを書いています。2004年10月から5年間連続で、Microsoft Most Valueable Professional Award for ASP/ASP.NET を受賞させていただきました。コミュニティの皆様のおかげです。ありがとうございます。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/5077 2010/05/12 22:34

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング