書籍転載
文法からはじめるプログラミング言語Microsoft Visual C++入門

Visual C++でマルチスレッド・プログラミング
――第13章 並列処理〜マルチスレッドプログラミング(後編)――

WINGSプロジェクト 矢吹 太朗(監修 山田 祥寛)
2010/04/28
Page1 Page2 Page3 Page4

本コーナーは、日経BPソフトプレス発行の書籍『文法からはじめるプログラミング言語Microsoft Visual C++入門』の中から、特にInsider.NET読者に有用だと考えられる章や個所をInsider.NET編集部が選び、同社の許可を得て転載したものです。基本的に元の文章をそのまま転載していますが、レイアウト上の理由などで文章の記述を変更している部分(例:「上の図」など)や、図の位置などを本サイトのデザインに合わせている部分が若干ありますので、ご了承ください。『文法からはじめるプログラミング言語Microsoft Visual C++入門』の詳細は「目次情報ページ」もしくは日経BPソフトプレスのサイトをご覧ください。

ご注意:本記事は、書籍の内容を改変することなく、そのまま転載したものです。このため用字用語の統一ルールなどは@ITのそれとは一致しません。あらかじめご了承ください。

13.3 標準C++におけるマルチスレッド

 マルチスレッドのためのクラスであるBoostのthreadを利用する方法を紹介します。前節で述べたように、BoostはWindowsのスレッドとUnix系OSのスレッド(pthread)を抽象化しているため、この節で作成するプログラムはUnix環境でもそのまま動作します。

13.3.1 スレッドの利用

 前節で述べた方針に従って、素数を列挙するプログラムを並列化します。

 まず、スレッドの処理は関数として実装します。3の倍数+1となる数だけを調べる関数threadFuncA()は次のようになります(この関数には不備があるので後で修正する必要があります)。

//3の倍数+1となる数だけを調べる
void threadFuncA()
{
  for (int n=4; n<=N; n+=3) {
    if (isPrime(n)) { //素数だったら表示
      cout<<n<<' ';
    }
  }
}
[サンプル]13-primes-boost1.cpp

 同様に、3の倍数+2となる数だけを調べる関数threadFuncB()は次のようになります。

//3の倍数+2となる数だけを調べる
void threadFuncB()
{
  for (int n=5; n<=N; n+=3) {
    if (isPrime(n)) { //素数だったら表示
      cout<<n<<' ';
    }
  }
}
[サンプル]13-primes-boost1.cpp

 次のような構文で関数を実行するスレッドを生成します。

thread 変数名(関数);
[構文]スレッドの生成

 これらの関数をスレッドで実行させるプログラムの関数main()は次のようになります(2と3は調べずに表示させています)。

int main()
{
  cout<<"2 3 ";

  thread threadA(threadFuncA);
  thread threadB(threadFuncB);

  //スレッドの終了を待つ
  threadA.join();
  threadB.join();
}
[サンプル]13-primes-boost1.cpp

 メソッドjoin()でスレッドの終了を待ってからプログラムを終了させなければなりません。スレッドの終了を待たずにプログラムが終了すると、計算途中のスレッドが強制終了させられるので、正しい結果が得られません。このように、スレッドの動作をそろえることを「同期を取る」と言います。

 2からNまでの素数を求めるプログラムの全体は次のようになります。

//共通言語ランタイムサポートを使用しない
#include <boost/thread.hpp>
#include "number.h"
using namespace std;
using namespace boost;

const int N=100;

(関数threadFuncA()の定義)

(関数threadFuncB()の定義)

(関数main()の定義)
[サンプル]13-primes-boost1.cpp

 このコードは共通言語ランタイムサポートを利用しているとコンパイルできないので、プロジェクトのプロパティで、「共通言語ランタイムサポートを利用しない」ように設定します(2.2.1項を参照)。

 ビルドして実行すると、次のような結果になります(環境によって結果は変わります)。

2 3 57  1113  1719  2331  2937  4143  4761  5367  5973  7179  8397  89

 求めたのは100までの素数なので、この結果は明らかに間違っています(間違った結果が再現されない環境もあるかもしれませんが、以下で述べるように、ここで実行したプログラムには不備があります)。間違った原因は、標準出力のためのオブジェクトcoutに、2つのスレッドから同時に書き込んでいるためです。ここで利用しているオブジェクトcoutは、複数のスレッドから同時に書き込まれることを想定して作られてはいません。ですから、同時に複数のスレッドがcoutを利用することができないようにしなければなりません。

 あるオブジェクトを利用するスレッドを限定することを排他制御と言います。排他制御の実現にはmutexと呼ばれるオブジェクトを使います。図13-4のように、あるスレッドがmutexを使ってプログラムの一部をロックすると、別のスレッドは、ロックが解放されるまで待機しなければならなくなります。

図13-4 mutexによるオブジェクトcoutへの排他制御の実現

 オブジェクトcoutをロックするコードを以下に示します。

//共通言語ランタイムサポートを使用しない
#include <boost/thread.hpp>
#include "number.h"
using namespace std;
using namespace boost;

const int N=100;
mutex mtx; //ロックのためのオブジェクト

//3の倍数+1となる数だけを調べる
void threadFuncA()
{
  for (int n=4; n<=N; n+=3) {
    if (isPrime(n)) { //素数なら表示
      mutex::scoped_lock lock(mtx); //スコープ内をロック
      cout<<n<<' ';
    }
  }
}

//3の倍数+2となる数だけを調べる
void threadFuncB()
{
  for (int n=5; n<=N; n+=3) {
    if (isPrime(n)) { //素数なら表示
      mutex::scoped_lock lock(mtx); //スコープ内をロック
      cout<<n<<' ';
    }
  }
}

int main()
{
  cout<<"2 3 ";
 
  thread threadA(threadFuncA);
  thread threadB(threadFuncB);
 
  threadA.join();
  threadB.join();
}
[サンプル]13-primes-boost2.cpp

 mutex::scoped_lockによって、そのスコープ(この例ではif文の内部)がロックされます。スコープの終了とともにロックは解除されます。

 実行結果は次のようになり、2から100までの素数がすべて列挙されていることがわかります。複数のスレッドがどのような順番で実行されるかは決まっていないので、このように、ばらばらの順番で出力されます。3で割った余りが1(あるいは2)のものだけに限定すれば、正しい順番で出力されています。

2 3 7 13 19 31 37 43 5 11 17 23 29 41 47 53 61 67 73 79 97 59 71 83 89

 関数threadFuncA()とthreadFuncB()は、次のように1つにまとめることができます。

//共通言語ランタイムサポートを使用しない
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include "number.h"
using namespace std;
using namespace boost;

const int N=100;

//startから3つおきに素数かどうか調べる
void threadFunc(int start)
{
  static mutex mtx;

  for (int n=start; n<=N; n+=3) {
    if (isPrime(n)) { //素数なら表示
      mutex::scoped_lock lock(mtx);
      cout<<n<<' ';
    }
  }
}

int main()
{
  cout<<"2 3 ";
 
  thread threadA(bind(threadFunc, 4)); //4から調べる
  thread threadB(bind(threadFunc, 5)); //5から調べる

  threadA.join();
  threadB.join();
}
[サンプル]13-primes-boost3.cpp

 このプログラムでは、次のような構文で、関数threadFunc()の引数を限定して新しい関数を作っています*1。10.1.13項のコラムで紹介した関数オブジェクトを使ってもよいでしょう。

*1 厳密に言えば、作られるのは「新しい関数」ではありません。このことは静的変数が共通であることからもわかります。

bind(関数名, 限定する引数)
[構文]関数の引数を限定して新しい関数を作る


 INDEX
  [書籍転載]文法からはじめるプログラミング言語Microsoft Visual C++入門
  Visual C++でマルチスレッド・プログラミング
  1.標準C++におけるマルチスレッド
    2.デッドロック
    3..NETにおけるマルチスレッド
    4.OpenMP

インデックス・ページヘ 「文法からはじめるプログラミング言語Microsoft Visual C++入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間