【C++】標準ライブラリで並列処理をやってみる【std::thread】
並列処理
前回は,OpenMPというライブラリを使って並列処理を行いました.
しかし,実際に大きなプロジェクトで扱おうとした時に,あまり効果が現れず,むしろ並列化させる場所によっては遅くなってしまう事態に見舞われました.
そこで初心に帰って(?),C++11の標準ライブラリ「Thread」を使って並列処理をやってみよう,という考えに至りました.
備忘録的な部分がありますが,どうかお付き合いください.
std::thread
先ほども言いましたが,C++11から標準ライブラリのひとつとなりました.
ですので,これで書いたコードは,OpenMPみたく下準備的なものが多分いらないはずです.
気をつけるべきこととしては,コンパイル時に,「-std=c++11」(14でも17でも良い)をつけることでしょうか.
私はCMakeListsで管理しているので,さほど大きな問題ではありません.
さっそく試しにコードを書いていきましょう.
サンプルコード
CMakeLists.txt
今回は仮にプロジェクト名は「parallel_sample」としています.
1 2 3 4 5 6 | cmake_minimum_required(VERSION 3.14) # なんでもよい project(parallel_sample) set(CMAKE_CXX_STANDARD 11) # C++11以降を指定 add_executable(parallel_sample main.cpp) |
その1:意味なし最低限コード
まずは必要最低限のコードから.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream> #include <thread> #include <vector> using namespace std; int main() { vector<thread> threads; cout << "最大スレッド数: " << thread::hardware_concurrency() << endl; for( int i = 0; i < 100; ++i ){ threads.emplace_back( // 何かしらの処理をスレッドに投げる [](){} ); } /// スレッドの同期待ち for(auto& t : threads) { t.join(); } return 0; } |
「何かしらの処理」を今回は,何もしないラムダ式で書いていますが,もちろん関数でもいいわけです.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <iostream> #include <thread> #include <vector> using namespace std; void func(){} // 何かしらの関数 int main() { vector<thread> threads; cout << "最大スレッド数: " << thread::hardware_concurrency() << endl; for( int i = 0; i < 100; ++i ){ threads.emplace_back( // 何かしらの処理をスレッドに投げる func ); } /// スレッドの同期待ち for(auto& t : threads) { t.join(); } return 0; } |
その2:共有変数ある場合
スレッド間で,共有の変数を扱う場合は「std::mutex」を使います.
書き方は主に2種類あって,どちらもミューテックスをロックして,用が済んだらアンロックする作業をしているだけです.
lock()とunlock()を明示的に書く場合は,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | #include <iostream> #include <thread> #include <vector> #include <mutex> /// new using namespace std; mutex mtx; //! ミューテックス int sum = 0; //! 共有する変数 void func(){ mtx.lock(); sum += 1; mtx.unlock(); } int main() { vector<thread> threads; cout << "最大スレッド数: " << thread::hardware_concurrency() << endl; for( int i = 0; i < 100; ++i ){ threads.emplace_back( // 何かしらの処理をスレッドに投げる func ); } /// スレッドの同期待ち for(auto& t : threads) { t.join(); } cout << sum << endl; return 0; } |
lock_guardクラスを用いる場合は,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <iostream> #include <thread> #include <vector> #include <mutex> /// new using namespace std; mutex mtx; //! ミューテックス int sum = 0; //! 共有する変数 void func(){ lock_guard<mutex> lock(mtx); /// ここが違う sum += 1; } int main() { vector<thread> threads; cout << "最大スレッド数: " << thread::hardware_concurrency() << endl; for( int i = 0; i < 100; ++i ){ threads.emplace_back( // 何かしらの処理をスレッドに投げる func ); } /// スレッドの同期待ち for(auto& t : threads) { t.join(); } cout << sum << endl; return 0; } |
のように書きます.
lock_guardは,コンストラクタでlockして,デストラクタでunlockするので,そのブロックスコープ全体(lock_guardが破棄されるまで)が排他制御となる.
試していないので分からないが,前者の方が早そうな気はする.
多分あんまり変わらない?
その3:クラスで扱う場合
クラスでも書き方は特に変わらない.
ミューテックスはメンバ変数として保持しておいて良い.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #include <iostream> #include <thread> #include <mutex> #include <vector> using namespace std; class Sample{ private: int sum; //! 共有変数 mutex mtx; //! ミューテックス public: Sample(): sum(0){}; // コンストラクタ static int calc(int x){ // 並列処理させる関数 return 2 * x; } void parallel(){ // 並列処理を行う本体 vector<thread> threads; for( int i = 0; i < 1000; ++i ){ threads.emplace_back( [i, this](){ mtx.lock(); sum += calc(i); mtx.unlock(); } ); } for(auto& t : threads) { t.join(); } } int get_Sum(){ // sumのgetter return sum; } }; int main() { Sample c; c.parallel(); cout << c.get_Sum() << endl; return 0; } |
こんな感じ.
並列処理と関係ないけど,メンバ関数内でラムダ式を書く時に,thisを忘れがち.
その4:他クラスのメンバ関数を並列処理
もしかしたら,他クラスのメンバ関数をスレッドに投げたいかもしれない.
そんなときは,以下のように書ける.
1 2 3 4 5 6 7 8 9 10 11 12 | void parallel(){ // 並列処理を行う本体 vector<thread> threads; Another a; for( int i = 0; i < 1000; ++i ){ // 他クラスのメンバ関数と,インスタンスをセットで参照渡しする threads.emplace_back( &Another::func, &a ); } for(auto& t : threads) { t.join(); } } |
コメントに書いてある説明が適切かは分からないが,とりあえず書き方はこう.笑
もちろんラムダ式で書いてもOKだと思う.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void parallel(){ // 並列処理を行う本体 vector<thread> threads; Another a; for( int i = 0; i < 1000; ++i ){ threads.emplace_back( [&a](){ a.func(); } ); } for(auto& t : threads) { t.join(); } } |
追記:メンバ変数としてmutexを定義した場合コピーはできない
記事公開して30分で追記を書いてます.笑
なにかというと,クラスのメンバ変数としてmutexを定義すると,そのクラスはmutexクラスの定義上コピーができなくなります.
例えば,
1 2 | Sample s1; auto s2 = s1; |
とかはできない,ってことです.
なぜかというと, mutexはコピーも移動も許されていないようです.
すなわち,それをメンバ変数として持っているクラスも,コピーは不可になります.
これで,さきほどエラーに悩まされました.
なので,以下のようなインスタンス生成も不可能です.
1 | Sample s = Sample(); |
おわりに
今回も,最近僕がハマっている?並列処理についてまとめました.
まだ勉強中故に全て理解できておらず,もしかしたらもっとスマートな書き方があるのかもしれない.
個人的には,OpenMPよりこういう風に明示的に書く方が好きです.
ブラックボックスすぎるのは,便利な反面あまり勉強にならないから好きじゃないんですよね...