Successfully reported this slideshow.

C++ マルチスレッドプログラミング

74

Share

Loading in …3
×
1 of 97
1 of 97

C++ マルチスレッドプログラミング

74

Share

Download to read offline

Description

Ohotech 特盛 #10 ( http://ohotech.connpass.com/event/7517/ )で発表した資料です。

Transcript

  1. 1. 2014/08/30 C++ マルチスレッドプログラミング @hotwatermorning 1
  2. 2. 発表者自己紹介 ✤ @hotwatermorning ✤ Sapporo.cpp運営メンバー ✤ C++ポケットリファレンス執筆 ✤ DTMer ✤ (ゲームプログラミングはやったことない) 2
  3. 3. 発表用に用意したソース ✤ こちらに https://github.com/hotwatermorning/Ohotech10-SampleGame ✤ ライブラリのパスを設定してVisual Studio 2013でビルドすると、サンプルゲームがビルド できる 3
  4. 4. 本日のレシピ ✤ マルチスレッドプログラミングの概略 ✤ C++のスレッドライブラリ ✤ 実践「task_queueクラス」 4
  5. 5. マルチスレッドプログラミングの 概略 5
  6. 6. マルチスレッドプログラム ✤ スレッド( プログラム中の実行の流れ)を 複数もつプログラム ✤ マルチコアCPUの各コア上でスレッドを動かせ ば、同時に複数の処理を実行できる 6
  7. 7. void ThreadProcess1() { doSomething1(); } void ThreadProcess2() { doSomething2(); } int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess2); th1.join(); th2.join(); } 複数の実行の流れ 7
  8. 8. void ThreadProcess1() { doSomething1(); } void ThreadProcess2() { doSomething2(); } int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess2); th1.join(); th2.join(); } 複数の実行の流れ 8
  9. 9. void ThreadProcess1() { doSomething1(); } int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess1); th1.join(); th2.join(); } 複数の実行の流れ 9 同じ関数を渡してスレッドを作成すると
  10. 10. void ThreadProcess1() { doSomething1(); } 一つ関数が、2つのスレッドで別々に同時に実行される int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess1); th1.join(); th2.join(); } 複数の実行の流れ 10
  11. 11. ゲーム & マルチスレッド ✤ 非同期処理のため ✤ パフォーマンス向上のため 11
  12. 12. スレッドによる非同期処理 ✤ プログラムの中に別の実行の流れを作り、元々の 流れを止めずに処理を行う ✤ ファイルIO/ネットワークIO ✤ 時間のかかる処理を別のスレッドで動かして、UIの流れを止めないようにする ✤ 音楽の再生 ✤ ディスプレイの更新とは別のタイミングで発生するデバイスの要求に 迅速に対応する 12
  13. 13. スレッドによるパフォーマンス向上 ✤ マルチコアCPUの余っているコアを使って処理を 行う ✤ パス探索/AI思考ルーチン ✤ マルチスレッド化の方針 ✤ タスク並列 ✤ 機能ごとに処理を分割し、マルチスレッドで実行する ✤ データ並列 ✤ 単一の機能で処理するデータを分割し、マルチスレッドで実行する ✤ http://www.isus.jp/article/game-special/designing-ai-for-games-4/ 13
  14. 14. マルチスレッドの難点 ✤ スレッド間の同期処理/排他制御 ✤ シングルスレッドのプログラミングよりも難くなる ✤ データ競合/デッドロック ✤ 思うようにパフォーマンスが向上しないことも ✤ 処理を分割する粒度、排他制御の仕方が悪ければ逆に パフォーマンスを下げてしまう 14
  15. 15. 既存スレッドライブラリの利用 ✤ マルチスレッドプログラミングは、複雑になりや すく、並行処理にバグがあると発見が困難になる ✤ なので充分に検証されたより高級な仕組み(ライ ブラリや言語拡張)が存在している場合は、それ を使うほうが安心 ✤ TBB, PPL, Parallel STL, OpenMP, ... ✤ ただし、このような仕組みを利用するには実行 ファイルの他にランタイムのDLLが必要になった りすることがある 15
  16. 16. C++のスレッドライブラリ 16
  17. 17. C++のスレッド 17 ✤ (C++11という規格から) 標準規格にスレッドが定義され、 ✤ スレッドを扱うクラスや、マルチスレッドプログ ラミングを支援するクラスが用意された
  18. 18. 以前のC++ ✤ 標準規格にスレッドが定義されていなかったの で、C++の実装系やOSの定義をもとにマルチス レッドプログラムを書く必要があった。 ✤ 標準規格にライブラリも用意されていなかったの で、OSが用意しているスレッドライブラリや pthread, Boost.Threadなどを利用していた ✤ 現在でも、C++11準拠度の低いC++実装系を使用する場 合は、これらのライブラリを使用することになる 18
  19. 19. 標準規格に定義されたクラス ✤ std::thread ✤ std::mutex ✤ std::lock_guard/std::unique_lock ✤ std::condition_variable ✤ std::promise/std::future ✤ std::atomic etc,... 19
  20. 20. std::thread ✤ スレッドクラス ✤ スレッドを表し、管理する ✤ スレッドを作成する ✤ スレッドの終了を待機する ✤ スレッドを切り離す 20
  21. 21. スレッドの作成 ✤ std::threadクラスのコンストラクタに、関数や 関数オブジェクトを渡すと、 ✤ 新たにスレッドが作成され、 ✤ コンストラクタに渡した関数がそのスレッド上で 実行される 21
  22. 22. // 別スレッドで呼び出したい関数 void ThreadProcess() { std::cout << "Hello Thread World" << std::endl; } void foo() { // スレッドを起動 std::thread th(ThreadProcess); th.join(); } スレッドの作成 22
  23. 23. 様々な方法でスレッドを作成する void func1() {} void func2(int x, int y) {} void func3(double &data) {} // 関数を渡してスレッドを作成 std::thread th1(func1); // 引数も渡せる std::thread th2(func2, 5, 10); double value = 0.5; // 参照を渡すにはstd::refを使用する std::thread th3(func3, std::ref(value)); 23
  24. 24. // 関数呼び出し演算子を持つクラスのオブジェクト // (i.e., 関数オブジェクト) struct FuncObj { void operator()(std::string text) const { std::cout << text << std::endl; } } fobj; // 関数オブジェクトを渡してスレッドを起動できる std::thread th4(fobj, "Hello"); // ラムダ式を渡してスレッドを起動できる std::thread th5( []{ std::cout << "World" << std::endl; } ); スレッドを作成する 24
  25. 25. スレッドを作成する struct ClassX { void Process() {} }; ClassX x; // メンバ関数のアドレスとオブジェクトを渡すと、 // これを結びつけて(bindして) // スレッドを起動できる std::thread th7(&ClassX::Process, x); 25
  26. 26. void DoSomething() { //時間のかかる関数 Sleep(1000/*millisec*/); } void foo() { std::thread th(DoSomething); //スレッドを起動して //スレッドの終了を待機する th.join(); std::cout << "joined." << std::endl; } スレッドの終了を待機する 26 1秒後に`joined.`が出力される
  27. 27. スレッドを切り離す void bar() { std::thread th(DoSomething); // スレッドを切り離し th.detach(); std::cout << "detached." << std::endl; } // thが破棄されてもスレッドは動き続ける 27 即座に`detached.`が出力される
  28. 28. std::threadクラスの注意点 ✤ なにかスレッドを作成した後は、そのスレッドを 管理しているstd::threadクラスのオブジェクトが デストラクトされる前に、join() or detach()を 呼び出す必要がある ✤ 予期せぬバグや、パフォーマンス上の問題となりうるため 28 ✤ ムーブによって所有権を移動した場合は、移動先のオブ ジェクトで適切にjoin() or detach()する
  29. 29. std::mutexクラス ✤ 排他制御(Mutual Exclusion : 相互排除)を行 うクラス 29
  30. 30. 排他制御 30 ✤ データ競合を防ぐための仕組み ✤ デッドロックに注意する
  31. 31. 排他制御していないコード int counter = 0; void DoWork() { DoSomething(); ++counter; // 複数のスレッドから同時に更新 } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 31
  32. 32. 排他制御していないコード int counter = 0; void DoWork() { DoSomething(); ++counter; // 複数のスレッドから同時に更新 } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 32 合計が2000にならない
  33. 33. 排他制御について ✤ 複数のスレッドから同じメモリ領域に 同時にアクセスする場合の安全性 ✤ 読み込み/読み込み → 安全 ✤ 書き込み/書き込み → 未定義動作 ✤ 読み込み/書き込み → 未定義動作 33 詳しくは http://yohhoy.hatenablog.jp/entry/2013/12/15/204116
  34. 34. 排他制御していないコード int counter = 0; void DoWork() { DoSomething(); ++counter; 00A34E2E call DoSomething (0A310FFh) 00A34E33 mov eax,dword ptr ds:[00A41390h] 00A34E38 add eax,1 00A34E3B mov dword ptr ds:[00A41390h],eax (※コンパイラやコンパイルオプションによって 生成されるアセンブラは異なるのでこれは一例) } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 34
  35. 35. データ競合 ✤ 複数のスレッドで共有しているデータを同時に 変更すると、プログラムの整合性が保てなくなる ✤ これをデータ競合(Data Race)と呼ぶ 35
  36. 36. クリティカルセクション ✤ 複数のスレッドから同時にアクセスされてはなら ない領域 ✤ ここをMutexで排他制御し、整合性を保つように する 36
  37. 37. クリティカルセクション int counter = 0; void DoWork() { DoSomething(); //ここから ++counter; //ここまでは一つのスレッドしか入れないように } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 37
  38. 38. クリティカルセクション int counter = 0; std::mutex mtx; // ミューテックス変数を定義 void DoWork() { DoSomething(); mtx.lock(); ++counter;   mtx.unlock(); } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 38
  39. 39. クリティカルセクション int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); mtx.lock(); //ロック確保 ++counter;   mtx.unlock(); //ロック解放 } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 39
  40. 40. アトミック性 ✤ 排他制御されたクリティカルセクションは、他の スレッドからはアトミック(不可分)に実行され たように見える ✤ 複数の状態を変更しつつ、その途中の状態を他の スレッドから観測されたくない場合は、その範囲 を排他制御によってアトミックにする 40
  41. 41. struct Statistics { void AddData( int data ); int GetAverage() const; }; Statistics st; void AddTwoData() { st.AddData(10); st.AddData(20); } void DisplayAverage() { ここまで実行された状態で DisplayAverage()を呼び出すと std::cout << st.GetAverage() << std::endl; } 排他制御していないコード 41 GetAverage()は10を返す
  42. 42. struct Statistics { void AddData( int data ); int GetAverage() const; }; Statistics st; void AddTwoData() { st.AddData(10); st.AddData(20); } void DisplayAverage() { std::cout << st.GetAverage() << std::endl; } 排他制御していないコード 42 シングルスレッドプログラミング では観測されない状態
  43. 43. Statistics st; std::mutex mtx; void AddTwoData() { mtx.lock(); st.AddData(10); st.AddData(20); mtx.unlock(); } void DisplayAverage() { mtx.lock(); std::cout << st.GetAverage() << std::endl; mtx.unlock(); } 排他制御していないコード 43 ミューテックスによって アトミック性が保証される DisplayAverage()からは AddTwoDataの中途半端な状態 が観測されないようになる
  44. 44. デッドロック ✤ 2つ以上のスレッドが、お互いのロックを確保し ようとして処理が停止してしまう状態 ✤ これをデッドロックと呼ぶ ✤ この状態に陥ったスレッドは、回復することも、 自らスレッドを終わらせることもできなくなる 44
  45. 45. std::mutex m1, m2; void worker1() { m1.lock(); m2.lock(); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { m2.lock(); m1.lock(); DoSomethingB(); m1.unlock(); m2.unlock(); } デッドロック 45 タイミングが悪いことに、 2つのスレッドがそれぞれ 最初のロックを確保すると
  46. 46. std::mutex m1, m2; void worker1() { m1.lock(); m2.lock(); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { m2.lock(); m1.lock(); DoSomethingB(); m1.unlock(); m2.unlock(); } デッドロック 46 どちらのスレッドも 処理を進められなくなる
  47. 47. デッドロック ✤ C++標準規格のstd::mutex型では、実装系で 可能な場合はresource_deadlock_would_occur をエラーコードに設定したstd::system_error 例外を送出してくれるかもしれない。 ✤ ただしそれに頼るべきではなく、根本的に ロジックの見直しをするべき 47
  48. 48. std::mutex m1, m2; void worker1() { m1.lock(); m2.lock(); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { m1.lock(); m2.lock(); DoSomethingB(); m2.unlock(); m1.unlock(); } デッドロック 48 複数のロックを確保する場合は、 必ず同じ順番で確保するようにする
  49. 49. std::mutex m1, m2; void worker1() { std::lock(m1, m2); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { std::lock(m1, m2); DoSomethingB(); m1.unlock(); m2.unlock(); } デッドロック あるいはstd::lock()関数を使用する http://d.hatena.ne.jp/melpon/20121006/1349503776 49
  50. 50. boost::shared_mutex 50 ✤ Reader/Writerロックを実現する ✤ ある変数を保護するための排他制御を行いたい 時、その変数へのアクセスのほとんどが読み込み で書き込みが少ない時に使用する ✤ まだ標準規格には取り入れられていない
  51. 51. reader側 boost::shared_mutex mtx; std::string text; // 複数のスレッドで共有するデータ void reader() { for( ; ; ) { mtx.shared_lock(); std::string tmp = text; mtx.shared_unlock(); DoSomething(tmp); } } shared_lockは複数のスレッドで std::thread reader_thread1(reader); std::thread reader_thread2(reader); //... 51 同時に取得できる
  52. 52. void writer() { for( ; ; ) { writer側 std::string const tmp = GetNewText(); mtx.lock(); text = tmp; mtx.unlock(); } } std::thread writer_thread1(writer); std::thread writer_thread2(writer); //... 通常のロックは一つのスレッドしか 52 取得できない
  53. 53. std::lock_guard/std::unique_lock 53 ✤ std::mutexクラスのlock()とunlock()を常に対応 せさて管理するのは面倒 ✤ RAIIというイディオムを使用して、ロックの管理 を楽にするためのクラス ✤ RAIIについては先月の「プログラミング生放送+CLR/H +Sapporo.cpp 勉強会@札幌」でも発表した http://www.slideshare.net/hotwatermorning/cpp-resource-management
  54. 54. lock()/unlock()の問題点 int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); mtx.lock(); ++counter; // ←ここで複雑なことをして例外が // 発生したり、 mtx.unlock(); // ←ここでunlock()を忘れると、    // ロックが確保されたままになる }   // この例だと、次回の呼び出しでデッドロックする 54
  55. 55. template<class Mutex> class lock_guard { public: lock_guard(Mutex &m) : m_(&m) { m_->lock(); } ̃lock_guard() { m_->unlock(); } private: Mutex *m_; }; lock_guardの実装イメージ 55 このようなクラスがあると何が可能になるか
  56. 56. int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); // ロック対象の型をテンプレート引数に指定して // コンストラクタに対象のオブジェクトを渡すと lock_guard<std::mutex> lock(mtx); ++counter; } lock_guardを使用する 56
  57. 57. int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); // コンストラクタ内で自動的にmtxのロック確保 lock_guard<std::mutex> lock(mtx); ++counter; // スコープを抜ける時にlock変数のデストラクタが // 呼ばれ、ロックが解放される } lock_guardを使用する 57
  58. 58. int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); lock_guard<std::mutex> lock(mtx); ++counter; } lock_guardを使用する 58 Mutexのロックをオブジェクトの 寿命に紐付けて管理できる
  59. 59. std::lock_guard 59 ✤ このロックの仕組みを実装したクラス ✤ テンプレート引数には、BasicLockable要件を満 たす型を指定できる ✤ つまり、std::mutexに限らずlock()/unlock()メンバ関数を 持つ型ならなんでもいい ✤ スコープ内でlock_guardクラスのオブジェクト が生きている間、ロックを確保する
  60. 60. std::unique_lock 60 ✤ std::lock_guardクラスの機能に加えて、より高 機能な仕組みをサポートする ✤ 明示的にロックを解放する ✤ ロックの所有権をムーブする ✤ ロックの確保を試行し、成功か失敗かを取得する など ✤ スコープ内で単純なロックをかけたい場合は lock_guardを ✤ ロックをより柔軟に管理したい場合は unique_lockを使用するとよい
  61. 61. std::condition_variable ✤ 条件変数と呼ばれる機能を実装したクラス ✤ 条件変数はモニタという同期の手法を実現する ✤ 条件を満たすまで実行をブロックし、条件を満た した時に実行を再開する 61
  62. 62. ユースケース ✤ Thread1は、データの準備が完了するのを待機 ✤ Thread2は、データを準備し、完了したら Thread1に通知 62
  63. 63. std::condition_variable cond; std::mutex mtx; bool is_data_ready = false; // Thread1 void WaitForDataToProcess() { std::unique_lock<std::mutex> lock(mtx); // データが準備できるまで待機する cond.wait(lock, [&]{ return is_data_ready; }); // データが準備できたので処理開始 process_data(); } condition_variableの使用例 63
  64. 64. // Thread2 void PrepareDataForProcessing() { retrieve_data(); prepare_data(); // データを準備する boost::lock_guard<boost::mutex> lock(mtx); // データが準備できたら完了フラグをセットして is_data_ready = true;  // 待機状態のスレッドを起こす cond.notify_one(); } condition_variableの使用例 64
  65. 65. Producer/Consumerパターン ✤ 非同期処理のデザインパターンの一つ ✤ 条件変数を利用して実装できる 65
  66. 66. データを追加するデータを取り出す 66 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 キュー
  67. 67. 67 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 キューが満杯だと データを追加できない → キューが空くまで待機
  68. 68. 68 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 データが取り出されると スペースが空く
  69. 69. 69 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 空いたスペースに データを追加
  70. 70. 70 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4
  71. 71. 71 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 キューが空だと データを取り出せない → データが追加されるまで待機
  72. 72. 72 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 データを追加すると キューが空でなくなる
  73. 73. 73 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 追加されたデータをConsumer4 取り出す
  74. 74. 74 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4
  75. 75. template<class T> class LockedQueue { void enqueue(T val) { if(キューが満杯) { 待機; } // (1) キューの末尾にvalを追加; キューが空でなくなったら(2)に通知; } T dequeue() { if(キューが空) { 待機; } // (2) キューの先頭からデータを取り出し; 満杯だったキューが空いたら(1)に通知; } }; 擬似コード 75 enqueue用/dequeue用に それぞれ一つずつ条件変数を使う
  76. 76. std::promise/std::future ✤ Promiseパターンを実装したクラス ✤ あるタイミングで行った処理の結果を、 別のタイミングで取得するための仕組み ✤ マルチスレッドに限らず、非同期処理のための 仕組み 76
  77. 77. std::promise/std::future // 値をセットするためのpromiseクラスを用意 std::promise<int> p; // promiseクラスのオブジェクトから、 // 対応するfutureクラスのオブジェクトを作成 std::future<int> f = p.get_future(); // pとfは"Shared State"という状態を共有しており、 // これを通じてpからfへ値や例外を受け渡せる 77
  78. 78. void foo() { try { int result = DoProcess(); p.set_value(result); } catch(...) { p.set_exception( std::current_exception() ); } } int bar() { // fにセットされた値を返す。 // fに値がセットされていなければ、 // セットされるまでブロックする return f.get(); } 別の場所でデータを受け渡す 78
  79. 79. スレッドを使用しない非同期処理 int main() { foo(); // DoProcess(); std::cout << bar() << std::endl; //結果を取得 } 79
  80. 80. int main() { std::thread th(foo); // 別スレッドで処理 std::cout << bar() << std::endl; //結果を取得 th.join(); } スレッドを使用して非同期処理 80
  81. 81. std::atomic ✤ アトミック変数を実装したクラス ✤ アトミック変数へのアクセスは複数のスレッドか ら同時に行っても安全 81
  82. 82. ロックしなくても安全 std::atomic<int> counter(0); void DoWork() { DoSomething(); ++counter; } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 82
  83. 83. ロックしなくても安全 std::atomic<int> counter(0); void DoWork() { DoSomething(); ++counter; } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 83 合計が正しく2000になる
  84. 84. ロックしなくても安全 std::atomic<int> counter(0); void DoWork() { DoSomething(); ++counter; } void Worker() { 011A57B4 lock xadd dword ptr [ecx],eax (※コンパイラやコンパイルオプションによって 生成されるアセンブラは異なるのでこれは一例) for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 84
  85. 85. std::atomic ✤ アトミック変数によって、ロックを使用せずに マルチスレッドプログラムを記述できる ✤ これを利用したアルゴリズムは ロックフリーアルゴリズムと呼ばれる ✤ 詳しくは「プログラミングの魔導書 vol.3」参照 85
  86. 86. std::atomic使用上の注意 ✤ アトミック変数は、正しく使うのが難しいので 注意 ✤ ハードウェアアーキテクチャのメモリモデルを理解してい なければ、思わぬバグの原因となる ✤ 「リリースビルドだけでなぜか落ちる」 ✤ なので、同期処理に不安がある時はミューテック スでちゃんとロックを掛けて処理した方が良い 86
  87. 87. 実践「task_queueクラス」 87
  88. 88. 実践 ✤ ここまでに紹介した機能を使って、 マルチスレッドをサポートした タスクキュークラスを作成する 88
  89. 89. task_queueクラス int calculate(int x, int y) { /*...*/ } // 5スレッドを内部的に立ち上げてタスクキュー作成 task_queue tq(5); // 関数をキューに追加 // 結果を取得するためのfutureが返る std::future<int> result = tq.enqueue(calculate, 10, 20); // タスクキュー内のいずれかのスレッドで // 関数が実行される // 実行結果を取得 std::cout << result.get() << std::endl; 89
  90. 90. 1.関数を追加3. タスクを実行 90 task_queueの動作 タスクキュー のスレッド1 ユーザー側の スレッド1 キュー タスクキュー のスレッド2 タスクキュー のスレッド3 タスクキュー のスレッド4 ユーザー側の スレッド2 ユーザー側の スレッド3 ユーザー側の スレッド4 task_queueクラス 2.「タスク」 として保持
  91. 91. //! タスクキューで扱うタスクを表すベースクラス struct task_base { virtual ̃task_base() {} virtual void run() = 0; }; template<class Ret, class F, class... Args> class task_impl : public task_base { task_impl(std::promise<Ret>&& p, F&& f, Args&&... args); // f(args...)を呼び出してpromiseに値を設定 void run() override final; }; タスクキューの実装イメージ 91
  92. 92. class task_queue { // Producer/Consumerパターンを実装したキュー locked_queue<std::unique_ptr<task_base>> queue_; template<class F, class... Args> std::future<F(Args...)の戻り値の型> enqueue(F &&f, Args&&... args) { std::promise<F(Args...)の戻り値の型> p; auto f = p.get_future(); std::unique_ptr<Task> ptask( new Task(std::move(p), f, args...) ); queue_.enqueue(std::move(ptask)); return f; } 92
  93. 93. // 続き void process() { for( ; ; ) { // タスクが積まれていたら、 // キューから取り出す std::unique_ptr<Task> task = locked_queue_.dequeue(); task->run(); // F(Args...)を呼び出し // promiseに値を設定する } } // 各スレッドがprocess()を実行する std::vector<std::thread> threads_; }; 93
  94. 94. 実装したソース ✤ 今回の発表用のサンプルソースのtask ディレクトリ ✤ 機能が追加されているので、前述の実装イメージ よりも複雑になっているが、やっていることは ほぼ同じ 94
  95. 95. task_queueを使用 ✤ デモ(サンプルゲーム) ✤ 複数のパーティクルの移動(計算負荷を高くして ある)にtask_queueクラスを使用し、並列処理 をする ✤ サンプルゲームは急ごしらえのため、 ソースの品質についてはご容赦ください 95
  96. 96. まとめ ✤ C++に標準で用意されたクラスを利用してマルチ スレッドプログラムを作成できる 96 ✤ マルチスレッドによって、パフォーマンスを向上 できる ✤ マルチスレッドプログラミングでは、 データ競合/デッドロックに注意する (そのための仕組みも用意されている)
  97. 97. 参考文献 ✤ 「Windowsプロフェッショナルゲームプログラミング2」秀和システム ISBN: 4798006033 ✤ 「The Art of Multiprocessor Programming 並行プログラミングの原理から実践まで」 アスキー・メディアワー クス ISBN: 4048679880 ✤ 「プログラミングの魔導書 ~Programmers' Grimoire~ Vol.3 “Parallel, Concurrent, and Distributed Programming”」株式会社 ロングゲート ISBN:978-4-9905296-5-9 ✤ 「C++ポケットリファレンス」 技術評論社 ISBN: 4774157155 ✤ 「並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング」 オライリージャパン ISBN: 4873114357 97

Description

Ohotech 特盛 #10 ( http://ohotech.connpass.com/event/7517/ )で発表した資料です。

Transcript

  1. 1. 2014/08/30 C++ マルチスレッドプログラミング @hotwatermorning 1
  2. 2. 発表者自己紹介 ✤ @hotwatermorning ✤ Sapporo.cpp運営メンバー ✤ C++ポケットリファレンス執筆 ✤ DTMer ✤ (ゲームプログラミングはやったことない) 2
  3. 3. 発表用に用意したソース ✤ こちらに https://github.com/hotwatermorning/Ohotech10-SampleGame ✤ ライブラリのパスを設定してVisual Studio 2013でビルドすると、サンプルゲームがビルド できる 3
  4. 4. 本日のレシピ ✤ マルチスレッドプログラミングの概略 ✤ C++のスレッドライブラリ ✤ 実践「task_queueクラス」 4
  5. 5. マルチスレッドプログラミングの 概略 5
  6. 6. マルチスレッドプログラム ✤ スレッド( プログラム中の実行の流れ)を 複数もつプログラム ✤ マルチコアCPUの各コア上でスレッドを動かせ ば、同時に複数の処理を実行できる 6
  7. 7. void ThreadProcess1() { doSomething1(); } void ThreadProcess2() { doSomething2(); } int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess2); th1.join(); th2.join(); } 複数の実行の流れ 7
  8. 8. void ThreadProcess1() { doSomething1(); } void ThreadProcess2() { doSomething2(); } int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess2); th1.join(); th2.join(); } 複数の実行の流れ 8
  9. 9. void ThreadProcess1() { doSomething1(); } int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess1); th1.join(); th2.join(); } 複数の実行の流れ 9 同じ関数を渡してスレッドを作成すると
  10. 10. void ThreadProcess1() { doSomething1(); } 一つ関数が、2つのスレッドで別々に同時に実行される int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess1); th1.join(); th2.join(); } 複数の実行の流れ 10
  11. 11. ゲーム & マルチスレッド ✤ 非同期処理のため ✤ パフォーマンス向上のため 11
  12. 12. スレッドによる非同期処理 ✤ プログラムの中に別の実行の流れを作り、元々の 流れを止めずに処理を行う ✤ ファイルIO/ネットワークIO ✤ 時間のかかる処理を別のスレッドで動かして、UIの流れを止めないようにする ✤ 音楽の再生 ✤ ディスプレイの更新とは別のタイミングで発生するデバイスの要求に 迅速に対応する 12
  13. 13. スレッドによるパフォーマンス向上 ✤ マルチコアCPUの余っているコアを使って処理を 行う ✤ パス探索/AI思考ルーチン ✤ マルチスレッド化の方針 ✤ タスク並列 ✤ 機能ごとに処理を分割し、マルチスレッドで実行する ✤ データ並列 ✤ 単一の機能で処理するデータを分割し、マルチスレッドで実行する ✤ http://www.isus.jp/article/game-special/designing-ai-for-games-4/ 13
  14. 14. マルチスレッドの難点 ✤ スレッド間の同期処理/排他制御 ✤ シングルスレッドのプログラミングよりも難くなる ✤ データ競合/デッドロック ✤ 思うようにパフォーマンスが向上しないことも ✤ 処理を分割する粒度、排他制御の仕方が悪ければ逆に パフォーマンスを下げてしまう 14
  15. 15. 既存スレッドライブラリの利用 ✤ マルチスレッドプログラミングは、複雑になりや すく、並行処理にバグがあると発見が困難になる ✤ なので充分に検証されたより高級な仕組み(ライ ブラリや言語拡張)が存在している場合は、それ を使うほうが安心 ✤ TBB, PPL, Parallel STL, OpenMP, ... ✤ ただし、このような仕組みを利用するには実行 ファイルの他にランタイムのDLLが必要になった りすることがある 15
  16. 16. C++のスレッドライブラリ 16
  17. 17. C++のスレッド 17 ✤ (C++11という規格から) 標準規格にスレッドが定義され、 ✤ スレッドを扱うクラスや、マルチスレッドプログ ラミングを支援するクラスが用意された
  18. 18. 以前のC++ ✤ 標準規格にスレッドが定義されていなかったの で、C++の実装系やOSの定義をもとにマルチス レッドプログラムを書く必要があった。 ✤ 標準規格にライブラリも用意されていなかったの で、OSが用意しているスレッドライブラリや pthread, Boost.Threadなどを利用していた ✤ 現在でも、C++11準拠度の低いC++実装系を使用する場 合は、これらのライブラリを使用することになる 18
  19. 19. 標準規格に定義されたクラス ✤ std::thread ✤ std::mutex ✤ std::lock_guard/std::unique_lock ✤ std::condition_variable ✤ std::promise/std::future ✤ std::atomic etc,... 19
  20. 20. std::thread ✤ スレッドクラス ✤ スレッドを表し、管理する ✤ スレッドを作成する ✤ スレッドの終了を待機する ✤ スレッドを切り離す 20
  21. 21. スレッドの作成 ✤ std::threadクラスのコンストラクタに、関数や 関数オブジェクトを渡すと、 ✤ 新たにスレッドが作成され、 ✤ コンストラクタに渡した関数がそのスレッド上で 実行される 21
  22. 22. // 別スレッドで呼び出したい関数 void ThreadProcess() { std::cout << "Hello Thread World" << std::endl; } void foo() { // スレッドを起動 std::thread th(ThreadProcess); th.join(); } スレッドの作成 22
  23. 23. 様々な方法でスレッドを作成する void func1() {} void func2(int x, int y) {} void func3(double &data) {} // 関数を渡してスレッドを作成 std::thread th1(func1); // 引数も渡せる std::thread th2(func2, 5, 10); double value = 0.5; // 参照を渡すにはstd::refを使用する std::thread th3(func3, std::ref(value)); 23
  24. 24. // 関数呼び出し演算子を持つクラスのオブジェクト // (i.e., 関数オブジェクト) struct FuncObj { void operator()(std::string text) const { std::cout << text << std::endl; } } fobj; // 関数オブジェクトを渡してスレッドを起動できる std::thread th4(fobj, "Hello"); // ラムダ式を渡してスレッドを起動できる std::thread th5( []{ std::cout << "World" << std::endl; } ); スレッドを作成する 24
  25. 25. スレッドを作成する struct ClassX { void Process() {} }; ClassX x; // メンバ関数のアドレスとオブジェクトを渡すと、 // これを結びつけて(bindして) // スレッドを起動できる std::thread th7(&ClassX::Process, x); 25
  26. 26. void DoSomething() { //時間のかかる関数 Sleep(1000/*millisec*/); } void foo() { std::thread th(DoSomething); //スレッドを起動して //スレッドの終了を待機する th.join(); std::cout << "joined." << std::endl; } スレッドの終了を待機する 26 1秒後に`joined.`が出力される
  27. 27. スレッドを切り離す void bar() { std::thread th(DoSomething); // スレッドを切り離し th.detach(); std::cout << "detached." << std::endl; } // thが破棄されてもスレッドは動き続ける 27 即座に`detached.`が出力される
  28. 28. std::threadクラスの注意点 ✤ なにかスレッドを作成した後は、そのスレッドを 管理しているstd::threadクラスのオブジェクトが デストラクトされる前に、join() or detach()を 呼び出す必要がある ✤ 予期せぬバグや、パフォーマンス上の問題となりうるため 28 ✤ ムーブによって所有権を移動した場合は、移動先のオブ ジェクトで適切にjoin() or detach()する
  29. 29. std::mutexクラス ✤ 排他制御(Mutual Exclusion : 相互排除)を行 うクラス 29
  30. 30. 排他制御 30 ✤ データ競合を防ぐための仕組み ✤ デッドロックに注意する
  31. 31. 排他制御していないコード int counter = 0; void DoWork() { DoSomething(); ++counter; // 複数のスレッドから同時に更新 } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 31
  32. 32. 排他制御していないコード int counter = 0; void DoWork() { DoSomething(); ++counter; // 複数のスレッドから同時に更新 } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 32 合計が2000にならない
  33. 33. 排他制御について ✤ 複数のスレッドから同じメモリ領域に 同時にアクセスする場合の安全性 ✤ 読み込み/読み込み → 安全 ✤ 書き込み/書き込み → 未定義動作 ✤ 読み込み/書き込み → 未定義動作 33 詳しくは http://yohhoy.hatenablog.jp/entry/2013/12/15/204116
  34. 34. 排他制御していないコード int counter = 0; void DoWork() { DoSomething(); ++counter; 00A34E2E call DoSomething (0A310FFh) 00A34E33 mov eax,dword ptr ds:[00A41390h] 00A34E38 add eax,1 00A34E3B mov dword ptr ds:[00A41390h],eax (※コンパイラやコンパイルオプションによって 生成されるアセンブラは異なるのでこれは一例) } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 34
  35. 35. データ競合 ✤ 複数のスレッドで共有しているデータを同時に 変更すると、プログラムの整合性が保てなくなる ✤ これをデータ競合(Data Race)と呼ぶ 35
  36. 36. クリティカルセクション ✤ 複数のスレッドから同時にアクセスされてはなら ない領域 ✤ ここをMutexで排他制御し、整合性を保つように する 36
  37. 37. クリティカルセクション int counter = 0; void DoWork() { DoSomething(); //ここから ++counter; //ここまでは一つのスレッドしか入れないように } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 37
  38. 38. クリティカルセクション int counter = 0; std::mutex mtx; // ミューテックス変数を定義 void DoWork() { DoSomething(); mtx.lock(); ++counter;   mtx.unlock(); } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 38
  39. 39. クリティカルセクション int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); mtx.lock(); //ロック確保 ++counter;   mtx.unlock(); //ロック解放 } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 39
  40. 40. アトミック性 ✤ 排他制御されたクリティカルセクションは、他の スレッドからはアトミック(不可分)に実行され たように見える ✤ 複数の状態を変更しつつ、その途中の状態を他の スレッドから観測されたくない場合は、その範囲 を排他制御によってアトミックにする 40
  41. 41. struct Statistics { void AddData( int data ); int GetAverage() const; }; Statistics st; void AddTwoData() { st.AddData(10); st.AddData(20); } void DisplayAverage() { ここまで実行された状態で DisplayAverage()を呼び出すと std::cout << st.GetAverage() << std::endl; } 排他制御していないコード 41 GetAverage()は10を返す
  42. 42. struct Statistics { void AddData( int data ); int GetAverage() const; }; Statistics st; void AddTwoData() { st.AddData(10); st.AddData(20); } void DisplayAverage() { std::cout << st.GetAverage() << std::endl; } 排他制御していないコード 42 シングルスレッドプログラミング では観測されない状態
  43. 43. Statistics st; std::mutex mtx; void AddTwoData() { mtx.lock(); st.AddData(10); st.AddData(20); mtx.unlock(); } void DisplayAverage() { mtx.lock(); std::cout << st.GetAverage() << std::endl; mtx.unlock(); } 排他制御していないコード 43 ミューテックスによって アトミック性が保証される DisplayAverage()からは AddTwoDataの中途半端な状態 が観測されないようになる
  44. 44. デッドロック ✤ 2つ以上のスレッドが、お互いのロックを確保し ようとして処理が停止してしまう状態 ✤ これをデッドロックと呼ぶ ✤ この状態に陥ったスレッドは、回復することも、 自らスレッドを終わらせることもできなくなる 44
  45. 45. std::mutex m1, m2; void worker1() { m1.lock(); m2.lock(); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { m2.lock(); m1.lock(); DoSomethingB(); m1.unlock(); m2.unlock(); } デッドロック 45 タイミングが悪いことに、 2つのスレッドがそれぞれ 最初のロックを確保すると
  46. 46. std::mutex m1, m2; void worker1() { m1.lock(); m2.lock(); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { m2.lock(); m1.lock(); DoSomethingB(); m1.unlock(); m2.unlock(); } デッドロック 46 どちらのスレッドも 処理を進められなくなる
  47. 47. デッドロック ✤ C++標準規格のstd::mutex型では、実装系で 可能な場合はresource_deadlock_would_occur をエラーコードに設定したstd::system_error 例外を送出してくれるかもしれない。 ✤ ただしそれに頼るべきではなく、根本的に ロジックの見直しをするべき 47
  48. 48. std::mutex m1, m2; void worker1() { m1.lock(); m2.lock(); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { m1.lock(); m2.lock(); DoSomethingB(); m2.unlock(); m1.unlock(); } デッドロック 48 複数のロックを確保する場合は、 必ず同じ順番で確保するようにする
  49. 49. std::mutex m1, m2; void worker1() { std::lock(m1, m2); DoSomethingA(); m2.unlock(); m1.unlock(); } void worker2() { std::lock(m1, m2); DoSomethingB(); m1.unlock(); m2.unlock(); } デッドロック あるいはstd::lock()関数を使用する http://d.hatena.ne.jp/melpon/20121006/1349503776 49
  50. 50. boost::shared_mutex 50 ✤ Reader/Writerロックを実現する ✤ ある変数を保護するための排他制御を行いたい 時、その変数へのアクセスのほとんどが読み込み で書き込みが少ない時に使用する ✤ まだ標準規格には取り入れられていない
  51. 51. reader側 boost::shared_mutex mtx; std::string text; // 複数のスレッドで共有するデータ void reader() { for( ; ; ) { mtx.shared_lock(); std::string tmp = text; mtx.shared_unlock(); DoSomething(tmp); } } shared_lockは複数のスレッドで std::thread reader_thread1(reader); std::thread reader_thread2(reader); //... 51 同時に取得できる
  52. 52. void writer() { for( ; ; ) { writer側 std::string const tmp = GetNewText(); mtx.lock(); text = tmp; mtx.unlock(); } } std::thread writer_thread1(writer); std::thread writer_thread2(writer); //... 通常のロックは一つのスレッドしか 52 取得できない
  53. 53. std::lock_guard/std::unique_lock 53 ✤ std::mutexクラスのlock()とunlock()を常に対応 せさて管理するのは面倒 ✤ RAIIというイディオムを使用して、ロックの管理 を楽にするためのクラス ✤ RAIIについては先月の「プログラミング生放送+CLR/H +Sapporo.cpp 勉強会@札幌」でも発表した http://www.slideshare.net/hotwatermorning/cpp-resource-management
  54. 54. lock()/unlock()の問題点 int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); mtx.lock(); ++counter; // ←ここで複雑なことをして例外が // 発生したり、 mtx.unlock(); // ←ここでunlock()を忘れると、    // ロックが確保されたままになる }   // この例だと、次回の呼び出しでデッドロックする 54
  55. 55. template<class Mutex> class lock_guard { public: lock_guard(Mutex &m) : m_(&m) { m_->lock(); } ̃lock_guard() { m_->unlock(); } private: Mutex *m_; }; lock_guardの実装イメージ 55 このようなクラスがあると何が可能になるか
  56. 56. int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); // ロック対象の型をテンプレート引数に指定して // コンストラクタに対象のオブジェクトを渡すと lock_guard<std::mutex> lock(mtx); ++counter; } lock_guardを使用する 56
  57. 57. int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); // コンストラクタ内で自動的にmtxのロック確保 lock_guard<std::mutex> lock(mtx); ++counter; // スコープを抜ける時にlock変数のデストラクタが // 呼ばれ、ロックが解放される } lock_guardを使用する 57
  58. 58. int counter = 0; std::mutex mtx; void DoWork() { DoSomething(); lock_guard<std::mutex> lock(mtx); ++counter; } lock_guardを使用する 58 Mutexのロックをオブジェクトの 寿命に紐付けて管理できる
  59. 59. std::lock_guard 59 ✤ このロックの仕組みを実装したクラス ✤ テンプレート引数には、BasicLockable要件を満 たす型を指定できる ✤ つまり、std::mutexに限らずlock()/unlock()メンバ関数を 持つ型ならなんでもいい ✤ スコープ内でlock_guardクラスのオブジェクト が生きている間、ロックを確保する
  60. 60. std::unique_lock 60 ✤ std::lock_guardクラスの機能に加えて、より高 機能な仕組みをサポートする ✤ 明示的にロックを解放する ✤ ロックの所有権をムーブする ✤ ロックの確保を試行し、成功か失敗かを取得する など ✤ スコープ内で単純なロックをかけたい場合は lock_guardを ✤ ロックをより柔軟に管理したい場合は unique_lockを使用するとよい
  61. 61. std::condition_variable ✤ 条件変数と呼ばれる機能を実装したクラス ✤ 条件変数はモニタという同期の手法を実現する ✤ 条件を満たすまで実行をブロックし、条件を満た した時に実行を再開する 61
  62. 62. ユースケース ✤ Thread1は、データの準備が完了するのを待機 ✤ Thread2は、データを準備し、完了したら Thread1に通知 62
  63. 63. std::condition_variable cond; std::mutex mtx; bool is_data_ready = false; // Thread1 void WaitForDataToProcess() { std::unique_lock<std::mutex> lock(mtx); // データが準備できるまで待機する cond.wait(lock, [&]{ return is_data_ready; }); // データが準備できたので処理開始 process_data(); } condition_variableの使用例 63
  64. 64. // Thread2 void PrepareDataForProcessing() { retrieve_data(); prepare_data(); // データを準備する boost::lock_guard<boost::mutex> lock(mtx); // データが準備できたら完了フラグをセットして is_data_ready = true;  // 待機状態のスレッドを起こす cond.notify_one(); } condition_variableの使用例 64
  65. 65. Producer/Consumerパターン ✤ 非同期処理のデザインパターンの一つ ✤ 条件変数を利用して実装できる 65
  66. 66. データを追加するデータを取り出す 66 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 キュー
  67. 67. 67 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 キューが満杯だと データを追加できない → キューが空くまで待機
  68. 68. 68 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 データが取り出されると スペースが空く
  69. 69. 69 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 空いたスペースに データを追加
  70. 70. 70 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4
  71. 71. 71 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 キューが空だと データを取り出せない → データが追加されるまで待機
  72. 72. 72 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4 データを追加すると キューが空でなくなる
  73. 73. 73 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 追加されたデータをConsumer4 取り出す
  74. 74. 74 Producer/Consumerパターン Producer1 Consumer1 Producer2 Producer3 Producer4 Producer5 Consumer2 Consumer3 Consumer4
  75. 75. template<class T> class LockedQueue { void enqueue(T val) { if(キューが満杯) { 待機; } // (1) キューの末尾にvalを追加; キューが空でなくなったら(2)に通知; } T dequeue() { if(キューが空) { 待機; } // (2) キューの先頭からデータを取り出し; 満杯だったキューが空いたら(1)に通知; } }; 擬似コード 75 enqueue用/dequeue用に それぞれ一つずつ条件変数を使う
  76. 76. std::promise/std::future ✤ Promiseパターンを実装したクラス ✤ あるタイミングで行った処理の結果を、 別のタイミングで取得するための仕組み ✤ マルチスレッドに限らず、非同期処理のための 仕組み 76
  77. 77. std::promise/std::future // 値をセットするためのpromiseクラスを用意 std::promise<int> p; // promiseクラスのオブジェクトから、 // 対応するfutureクラスのオブジェクトを作成 std::future<int> f = p.get_future(); // pとfは"Shared State"という状態を共有しており、 // これを通じてpからfへ値や例外を受け渡せる 77
  78. 78. void foo() { try { int result = DoProcess(); p.set_value(result); } catch(...) { p.set_exception( std::current_exception() ); } } int bar() { // fにセットされた値を返す。 // fに値がセットされていなければ、 // セットされるまでブロックする return f.get(); } 別の場所でデータを受け渡す 78
  79. 79. スレッドを使用しない非同期処理 int main() { foo(); // DoProcess(); std::cout << bar() << std::endl; //結果を取得 } 79
  80. 80. int main() { std::thread th(foo); // 別スレッドで処理 std::cout << bar() << std::endl; //結果を取得 th.join(); } スレッドを使用して非同期処理 80
  81. 81. std::atomic ✤ アトミック変数を実装したクラス ✤ アトミック変数へのアクセスは複数のスレッドか ら同時に行っても安全 81
  82. 82. ロックしなくても安全 std::atomic<int> counter(0); void DoWork() { DoSomething(); ++counter; } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 82
  83. 83. ロックしなくても安全 std::atomic<int> counter(0); void DoWork() { DoSomething(); ++counter; } void Worker() { for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 83 合計が正しく2000になる
  84. 84. ロックしなくても安全 std::atomic<int> counter(0); void DoWork() { DoSomething(); ++counter; } void Worker() { 011A57B4 lock xadd dword ptr [ecx],eax (※コンパイラやコンパイルオプションによって 生成されるアセンブラは異なるのでこれは一例) for(int i = 0; i < 1000; ++i) { DoWork(); } } std::thread th1(Worker); std::thread th2(Worker); 84
  85. 85. std::atomic ✤ アトミック変数によって、ロックを使用せずに マルチスレッドプログラムを記述できる ✤ これを利用したアルゴリズムは ロックフリーアルゴリズムと呼ばれる ✤ 詳しくは「プログラミングの魔導書 vol.3」参照 85
  86. 86. std::atomic使用上の注意 ✤ アトミック変数は、正しく使うのが難しいので 注意 ✤ ハードウェアアーキテクチャのメモリモデルを理解してい なければ、思わぬバグの原因となる ✤ 「リリースビルドだけでなぜか落ちる」 ✤ なので、同期処理に不安がある時はミューテック スでちゃんとロックを掛けて処理した方が良い 86
  87. 87. 実践「task_queueクラス」 87
  88. 88. 実践 ✤ ここまでに紹介した機能を使って、 マルチスレッドをサポートした タスクキュークラスを作成する 88
  89. 89. task_queueクラス int calculate(int x, int y) { /*...*/ } // 5スレッドを内部的に立ち上げてタスクキュー作成 task_queue tq(5); // 関数をキューに追加 // 結果を取得するためのfutureが返る std::future<int> result = tq.enqueue(calculate, 10, 20); // タスクキュー内のいずれかのスレッドで // 関数が実行される // 実行結果を取得 std::cout << result.get() << std::endl; 89
  90. 90. 1.関数を追加3. タスクを実行 90 task_queueの動作 タスクキュー のスレッド1 ユーザー側の スレッド1 キュー タスクキュー のスレッド2 タスクキュー のスレッド3 タスクキュー のスレッド4 ユーザー側の スレッド2 ユーザー側の スレッド3 ユーザー側の スレッド4 task_queueクラス 2.「タスク」 として保持
  91. 91. //! タスクキューで扱うタスクを表すベースクラス struct task_base { virtual ̃task_base() {} virtual void run() = 0; }; template<class Ret, class F, class... Args> class task_impl : public task_base { task_impl(std::promise<Ret>&& p, F&& f, Args&&... args); // f(args...)を呼び出してpromiseに値を設定 void run() override final; }; タスクキューの実装イメージ 91
  92. 92. class task_queue { // Producer/Consumerパターンを実装したキュー locked_queue<std::unique_ptr<task_base>> queue_; template<class F, class... Args> std::future<F(Args...)の戻り値の型> enqueue(F &&f, Args&&... args) { std::promise<F(Args...)の戻り値の型> p; auto f = p.get_future(); std::unique_ptr<Task> ptask( new Task(std::move(p), f, args...) ); queue_.enqueue(std::move(ptask)); return f; } 92
  93. 93. // 続き void process() { for( ; ; ) { // タスクが積まれていたら、 // キューから取り出す std::unique_ptr<Task> task = locked_queue_.dequeue(); task->run(); // F(Args...)を呼び出し // promiseに値を設定する } } // 各スレッドがprocess()を実行する std::vector<std::thread> threads_; }; 93
  94. 94. 実装したソース ✤ 今回の発表用のサンプルソースのtask ディレクトリ ✤ 機能が追加されているので、前述の実装イメージ よりも複雑になっているが、やっていることは ほぼ同じ 94
  95. 95. task_queueを使用 ✤ デモ(サンプルゲーム) ✤ 複数のパーティクルの移動(計算負荷を高くして ある)にtask_queueクラスを使用し、並列処理 をする ✤ サンプルゲームは急ごしらえのため、 ソースの品質についてはご容赦ください 95
  96. 96. まとめ ✤ C++に標準で用意されたクラスを利用してマルチ スレッドプログラムを作成できる 96 ✤ マルチスレッドによって、パフォーマンスを向上 できる ✤ マルチスレッドプログラミングでは、 データ競合/デッドロックに注意する (そのための仕組みも用意されている)
  97. 97. 参考文献 ✤ 「Windowsプロフェッショナルゲームプログラミング2」秀和システム ISBN: 4798006033 ✤ 「The Art of Multiprocessor Programming 並行プログラミングの原理から実践まで」 アスキー・メディアワー クス ISBN: 4048679880 ✤ 「プログラミングの魔導書 ~Programmers' Grimoire~ Vol.3 “Parallel, Concurrent, and Distributed Programming”」株式会社 ロングゲート ISBN:978-4-9905296-5-9 ✤ 「C++ポケットリファレンス」 技術評論社 ISBN: 4774157155 ✤ 「並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング」 オライリージャパン ISBN: 4873114357 97

More Related Content

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

×