基本的な用語を覚えよう―ミューテックス
先ほどのプログラムでは、マルチスレッドによる並列実行ができたものの、出力文字列が混ざってしまいました。
それでは困る場合もありますので、複数のスレッド間で資源をうまく共有するための仕組みが必要となります。
Boost.Threadにはそのような目的のための機構としてミューテックス(Mutex:mutual exclusion:相互排他)が用意されています。
Boost.Threadのミューテックスにはさまざまな使い方があり、lock関数やunlock関数を使うもの、boost::mutex::scoped_lockクラスをローカル変数(局所変数)として生成してから破棄(その中かっこを出る)までロックするものなどがあります。
ミューテックスでの排他制御をすべき代表的な資源として、複数のスレッドから使用されるグローバル変数(大域変数)があります。この場合、グローバル変数1つあるいは関連するグローバル変数をまとめたグループに対して、1つのミューテックスを用意します。
各スレッドはグローバル変数をアクセスする場合、以下のようなルールに従ってグローバル変数へのアクセスを行います。
- ミューテックスをロックする
- グローバル変数の読み書きをする
- ミューテックスをアンロックする
もし、ほかのスレッドがこのグローバル変数にアクセスしていない場合、ミューテックスのロックはすぐに終了し、グローバル変数へのアクセスを行うことができます。
逆にほかのスレッドがこのグローバル変数にアクセスしている場合、ほかのスレッドがアンロックするまで待たされた後にロックが取得でき、グローバル変数にアクセスできます。
このような仕組みを使って、排他的なアクセスを実現できます。
次のコードは標準出力を共有資源と考えて排他制御を行った例です。ミューテックスによる排他制御を行わない場合には、各スレッドの出力する文字列が混ざってしまうことがあるのに対して、ミューテックスによる排他制御を行う場合にはそのようなことは起こりません。
#include <iostream> #include <string> #include <boost/thread.hpp> class ParaFunc { private: boost::mutex & m_mtx; const std::string m_str; public: ParaFunc(boost::mutex & m, const std::string & s) : m_mtx(m), m_str(s) {} void operator () (void) { boost::mutex::scoped_lock lock(m_mtx); for (int i = 0; i < 3; ++i) { std::cout << m_str << std::endl; } } }; int main(void) { boost::mutex mtx; ParaFunc pf0(mtx, "0"); ParaFunc pf1(mtx, "1"); ParaFunc pf2(mtx, "2"); boost::thread th0(pf0); boost::thread th1(pf1); boost::thread th2(pf2); th0.join(); th1.join(); th2.join(); return 0; }
sample_boost_mutex.cxxとして保存し、コンパイルします。
$ g++ sample_boost_mutex.cxx -lboost_thread -o sample_boost_mutex.elf
実行してみましょう。
$ ./sample_boost_mutex.elf 0 0 0 2 2 2 1 1 1
今回の結果は文字が混ざらず、排他制御が行われていることが分かると思います。
では、このプログラムの解説です。
main関数の先頭26行目、排他制御に必要となるboost::mutexクラスの変数mtxを定義します。この変数を使って各スレッドは共有資源の排他制御を行います。
次に28行目でスレッド化されるParaFuncクラスの変数を定義します。この時、各変数に対して先程定義したmtxを渡しておきます。
その後、main関数でboost::threadクラスの変数が定義されスレッドが生成される部分から先は同じですが、先程のpara_func関数に相当するのが、14行目のParaFuncクラスでvoid operator () (void)と定義されている関数です。boost::threadクラスの変数によりそれぞれ別スレッドとして実行が開始されます。
operetor ()関数の先頭ではboost::mutex::scoped_lockクラスの変数によってミューテックスのロックが取得され、続くforループで標準出力へ出力を行った後、operator ()関数がリターンするのに合わせて自動的にミューテックスがアンロックされます。
そのほかの基本的な用語
スレッドを用いた並列プログラムを書く場合、このほかにも従来のプログラムでは見慣れない用語が必要となります。それら基礎的な用語について説明します。
同期(syncronization)
複数のスレッドが動作する場合、各スレッドの作業が終わるのを待ってからでないと次の作業が始められない場合があります。このようなスレッドの待ち合わせを同期と呼びます。
レースコンディション(race conditions)
各スレッドは実行するコンピュータやオペレーティングシステムなどの外部要因によっては、一切邪魔されることなく処理を実行できたり、とぎれとぎれに処理を実行できたりします。すると、前回はうまく実行できたプログラムが、今回は間違った結果を出力してしまうというように、実行ごとに結果が変わってしまう場合があります。
このような実行ごとに結果が変わることをレースコンディションと呼びます。このようなことが起こらないように、資源の排他制御やスレッド間の同期を適切に行わなければなりません。
デッドロック(deadlocks)
同期や排他制御が適切に行われていない場合、各スレッドがそれぞれ別のスレッドを待ってしまい、結果的にすべてのスレッドが待ち状態のまま先に進めなくなってしまう場合があります。このような状況をデッドロックと呼びます。
並行(concurrent)
並列でも疑似並列でもいいが任意の順番で実行したい場合があります。このような概念を並行と呼びます。
次回は、並列プログラムの設計からスレッドによる並列化プログラミングの実際を解説します。
Proposed Draft Technical Report on C++ Library Extensions
はじめての並列プログラミング
マルチコアCPUのための並列プログラミング
Boost C++ Libraries Chapter 17. Thread
Patterns for Parallel Programming
2/2 |
Index | |
並列処理プログラミングの基本用語 | |
Page1 「並列化されていないコードよりは速く」から始めよう Boost C++ライブラリ 基本的な用語を覚えよう―スレッド |
|
Page2 基本的な用語を覚えよう―ミューテックス そのほかの基本的な用語 |
Think Parallelで行こう! |
- プログラムの実行はどのようにして行われるのか、Linuxカーネルのコードから探る (2017/7/20)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。最終回は、Linuxカーネルの中では、プログラムの起動時にはどのような処理が行われているのかを探る - エンジニアならC言語プログラムの終わりに呼び出されるexit()の中身分かってますよね? (2017/7/13)
C言語の「Hello World!」プログラムで使われる、「printf()」「main()」関数の中身を、デバッガによる解析と逆アセンブル、ソースコード読解などのさまざまな側面から探る連載。今回は、プログラムの終わりに呼び出されるexit()の中身を探る - VBAにおけるFileDialog操作の基本&ドライブの空き容量、ファイルのサイズやタイムスタンプの取得方法 (2017/7/10)
指定したドライブの空き容量、ファイルのタイムスタンプや属性を取得する方法、FileDialog/エクスプローラー操作の基本を紹介します - さらば残業! 面倒くさいエクセル業務を楽にする「Excel VBA」とは (2017/7/6)
日頃発生する“面倒くさい業務”。簡単なプログラミングで効率化できる可能性がある。本稿では、業務で使うことが多い「Microsoft Excel」で使えるVBAを紹介する。※ショートカットキー、アクセスキーの解説あり
|
|