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

97
2014/08/30 C++ マルチスレッドプログラミング @hotwatermorning 1

description

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

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

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

2014/08/30

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

@hotwatermorning

1

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

発表者自己紹介

✤ @hotwatermorning✤ Sapporo.cpp運営メンバー✤ C++ポケットリファレンス執筆✤ DTMer✤(ゲームプログラミングはやったことない)

2

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

発表用に用意したソース

✤こちらに https://github.com/hotwatermorning/Ohotech10-SampleGame✤ライブラリのパスを設定してVisual Studio 2013でビルドすると、サンプルゲームがビルドできる

3

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

本日のレシピ

✤マルチスレッドプログラミングの概略✤ C++のスレッドライブラリ✤実践「task_queueクラス」

4

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

マルチスレッドプログラミングの概略

5

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

マルチスレッドプログラム

✤スレッド( プログラム中の実行の流れ)を複数もつプログラム

✤マルチコアCPUの各コア上でスレッドを動かせば、同時に複数の処理を実行できる

6

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

void ThreadProcess1() { doSomething1();}

void ThreadProcess2() { doSomething2();}

int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess2);

th1.join(); th2.join();}

複数の実行の流れ

7

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

void ThreadProcess1() { doSomething1();}

void ThreadProcess2() { doSomething2();}

int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess2);

th1.join(); th2.join();}

複数の実行の流れ

8

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

void ThreadProcess1() { doSomething1();}

int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess1);

th1.join(); th2.join();}

複数の実行の流れ

9

同じ関数を渡してスレッドを作成すると

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

void ThreadProcess1() { doSomething1();}

int main() { std::thread th1(ThreadProcess1); std::thread th2(ThreadProcess1);

th1.join(); th2.join();}

複数の実行の流れ

10

一つ関数が、2つのスレッドで別々に同時に実行される

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

ゲーム & マルチスレッド

✤非同期処理のため✤パフォーマンス向上のため

11

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

スレッドによる非同期処理

✤プログラムの中に別の実行の流れを作り、元々の流れを止めずに処理を行う✤ ファイルIO/ネットワークIO

✤ 時間のかかる処理を別のスレッドで動かして、UIの流れを止めないようにする✤ 音楽の再生

✤ ディスプレイの更新とは別のタイミングで発生するデバイスの要求に迅速に対応する

12

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

スレッドによるパフォーマンス向上✤マルチコアCPUの余っているコアを使って処理を行う

✤パス探索/AI思考ルーチン✤マルチスレッド化の方針

✤ タスク並列✤ 機能ごとに処理を分割し、マルチスレッドで実行する

✤ データ並列✤ 単一の機能で処理するデータを分割し、マルチスレッドで実行する

✤ http://www.isus.jp/article/game-special/designing-ai-for-games-4/

13

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

マルチスレッドの難点

✤スレッド間の同期処理/排他制御✤ シングルスレッドのプログラミングよりも難くなる✤ データ競合/デッドロック

✤思うようにパフォーマンスが向上しないことも✤ 処理を分割する粒度、排他制御の仕方が悪ければ逆にパフォーマンスを下げてしまう

14

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

既存スレッドライブラリの利用✤マルチスレッドプログラミングは、複雑になりやすく、並行処理にバグがあると発見が困難になる

✤なので充分に検証されたより高級な仕組み(ライブラリや言語拡張)が存在している場合は、それを使うほうが安心✤ TBB, PPL, Parallel STL, OpenMP, ...

✤ただし、このような仕組みを利用するには実行ファイルの他にランタイムのDLLが必要になったりすることがある

15

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

C++のスレッドライブラリ

16

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

C++のスレッド

17

✤(C++11という規格から)標準規格にスレッドが定義され、

✤スレッドを扱うクラスや、マルチスレッドプログラミングを支援するクラスが用意された

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

以前のC++

✤標準規格にスレッドが定義されていなかったので、C++の実装系やOSの定義をもとにマルチスレッドプログラムを書く必要があった。

✤標準規格にライブラリも用意されていなかったので、OSが用意しているスレッドライブラリやpthread, Boost.Threadなどを利用していた✤ 現在でも、C++11準拠度の低いC++実装系を使用する場合は、これらのライブラリを使用することになる

18

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

標準規格に定義されたクラス✤ std::thread✤ std::mutex✤ std::lock_guard/std::unique_lock✤ std::condition_variable✤ std::promise/std::future✤ std::atomicetc,...

19

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

std::thread

✤スレッドクラス✤スレッドを表し、管理する

✤ スレッドを作成する✤ スレッドの終了を待機する✤ スレッドを切り離す

20

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

スレッドの作成

✤ std::threadクラスのコンストラクタに、関数や関数オブジェクトを渡すと、

✤新たにスレッドが作成され、✤コンストラクタに渡した関数がそのスレッド上で実行される

21

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

// 別スレッドで呼び出したい関数void ThreadProcess() { std::cout << "Hello Thread World" << std::endl;}

void foo() { // スレッドを起動 std::thread th(ThreadProcess);

th.join();}

スレッドの作成

22

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

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

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

// 関数呼び出し演算子を持つクラスのオブジェクト// (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

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

struct ClassX { void Process() {}};

ClassX x;

// メンバ関数のアドレスとオブジェクトを渡すと、// これを結びつけて(bindして)// スレッドを起動できるstd::thread th7(&ClassX::Process, x);

スレッドを作成する

25

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

void DoSomething() { //時間のかかる関数 Sleep(1000/*millisec*/);}

void foo() { std::thread th(DoSomething); //スレッドを起動して

//スレッドの終了を待機する th.join();

std::cout << "joined." << std::endl;}

スレッドの終了を待機する

26

1秒後に`joined.`が出力される

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

void bar() { std::thread th(DoSomething);

// スレッドを切り離し th.detach(); std::cout << "detached." << std::endl;

} // thが破棄されてもスレッドは動き続ける

スレッドを切り離す

27

即座に`detached.`が出力される

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

std::threadクラスの注意点

28

✤なにかスレッドを作成した後は、そのスレッドを管理しているstd::threadクラスのオブジェクトがデストラクトされる前に、join() or detach()を呼び出す必要がある✤ 予期せぬバグや、パフォーマンス上の問題となりうるため✤ ムーブによって所有権を移動した場合は、移動先のオブジェクトで適切にjoin() or detach()する

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

std::mutexクラス

✤排他制御(Mutual Exclusion : 相互排除)を行うクラス

29

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

排他制御

30

✤データ競合を防ぐための仕組み✤デッドロックに注意する

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

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

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

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にならない

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

排他制御について

✤複数のスレッドから同じメモリ領域に同時にアクセスする場合の安全性

✤読み込み/読み込み → 安全✤書き込み/書き込み → 未定義動作✤読み込み/書き込み → 未定義動作

33

詳しくは http://yohhoy.hatenablog.jp/entry/2013/12/15/204116

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

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);

排他制御していないコード

34

00A34E2E call DoSomething (0A310FFh) 

00A34E33 mov  eax,dword ptr ds:[00A41390h] 00A34E38 add  eax,1 00A34E3B mov  dword ptr ds:[00A41390h],eax  (※コンパイラやコンパイルオプションによって生成されるアセンブラは異なるのでこれは一例)

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

データ競合

✤複数のスレッドで共有しているデータを同時に変更すると、プログラムの整合性が保てなくなる

✤これをデータ競合(Data Race)と呼ぶ

35

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

クリティカルセクション

✤複数のスレッドから同時にアクセスされてはならない領域

✤ここをMutexで排他制御し、整合性を保つようにする

36

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

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

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

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

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

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

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

アトミック性

40

✤排他制御されたクリティカルセクションは、他のスレッドからはアトミック(不可分)に実行されたように見える

✤複数の状態を変更しつつ、その途中の状態を他のスレッドから観測されたくない場合は、その範囲を排他制御によってアトミックにする

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

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;}

排他制御していないコード

41

ここまで実行された状態でDisplayAverage()を呼び出すと

GetAverage()は10を返す

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

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

シングルスレッドプログラミングでは観測されない状態

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

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の中途半端な状態が観測されないようになる

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

デッドロック

✤ 2つ以上のスレッドが、お互いのロックを確保しようとして処理が停止してしまう状態

✤これをデッドロックと呼ぶ✤この状態に陥ったスレッドは、回復することも、自らスレッドを終わらせることもできなくなる

44

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

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つのスレッドがそれぞれ最初のロックを確保すると

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

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

どちらのスレッドも処理を進められなくなる

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

デッドロック

✤ C++標準規格のstd::mutex型では、実装系で可能な場合はresource_deadlock_would_occurをエラーコードに設定したstd::system_error例外を送出してくれるかもしれない。

✤ただしそれに頼るべきではなく、根本的にロジックの見直しをするべき

47

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

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

複数のロックを確保する場合は、必ず同じ順番で確保するようにする

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

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();}

デッドロック

49

あるいはstd::lock()関数を使用するhttp://d.hatena.ne.jp/melpon/20121006/1349503776

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

boost::shared_mutex

50

✤ Reader/Writerロックを実現する✤ある変数を保護するための排他制御を行いたい時、その変数へのアクセスのほとんどが読み込みで書き込みが少ない時に使用する

✤まだ標準規格には取り入れられていない

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

boost::shared_mutex mtx;std::string text; // 複数のスレッドで共有するデータvoid reader() { for( ; ; ) { mtx.shared_lock(); std::string tmp = text; mtx.shared_unlock(); DoSomething(tmp); }}

std::thread reader_thread1(reader);std::thread reader_thread2(reader);//...

reader側

51

shared_lockは複数のスレッドで同時に取得できる

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

void writer() { for( ; ; ) { std::string const tmp = GetNewText(); mtx.lock(); text = tmp; mtx.unlock(); }}std::thread writer_thread1(writer);std::thread writer_thread2(writer);//...

writer側

52

通常のロックは一つのスレッドしか取得できない

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

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

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

int counter = 0;std::mutex mtx;

void DoWork() { DoSomething();

mtx.lock(); ++counter; // ←ここで複雑なことをして例外が // 発生したり、 mtx.unlock(); // ←ここでunlock()を忘れると、   // ロックが確保されたままになる}   // この例だと、次回の呼び出しでデッドロックする

lock()/unlock()の問題点

54

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

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

このようなクラスがあると何が可能になるか

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

int counter = 0;std::mutex mtx;

void DoWork() { DoSomething(); // ロック対象の型をテンプレート引数に指定して // コンストラクタに対象のオブジェクトを渡すと lock_guard<std::mutex> lock(mtx); ++counter;

}

lock_guardを使用する

56

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

int counter = 0;std::mutex mtx;

void DoWork() { DoSomething();

// コンストラクタ内で自動的にmtxのロック確保 lock_guard<std::mutex> lock(mtx); ++counter; // スコープを抜ける時にlock変数のデストラクタが // 呼ばれ、ロックが解放される}

lock_guardを使用する

57

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

int counter = 0;std::mutex mtx;

void DoWork() { DoSomething();

lock_guard<std::mutex> lock(mtx); ++counter;

}

lock_guardを使用する

58

Mutexのロックをオブジェクトの寿命に紐付けて管理できる

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

std::lock_guard

59

✤このロックの仕組みを実装したクラス✤テンプレート引数には、BasicLockable要件を満たす型を指定できる✤ つまり、std::mutexに限らずlock()/unlock()メンバ関数を持つ型ならなんでもいい

✤スコープ内でlock_guardクラスのオブジェクトが生きている間、ロックを確保する

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

std::unique_lock

60

✤ std::lock_guardクラスの機能に加えて、より高機能な仕組みをサポートする✤ 明示的にロックを解放する✤ ロックの所有権をムーブする✤ ロックの確保を試行し、成功か失敗かを取得するなど

✤スコープ内で単純なロックをかけたい場合はlock_guardを

✤ロックをより柔軟に管理したい場合はunique_lockを使用するとよい

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

std::condition_variable

✤条件変数と呼ばれる機能を実装したクラス✤条件変数はモニタという同期の手法を実現する✤条件を満たすまで実行をブロックし、条件を満たした時に実行を再開する

61

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

ユースケース

✤ Thread1は、データの準備が完了するのを待機✤ Thread2は、データを準備し、完了したらThread1に通知

62

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

std::condition_variable cond;std::mutex mtx;bool is_data_ready = false;

// Thread1void WaitForDataToProcess() { std::unique_lock<std::mutex> lock(mtx);

// データが準備できるまで待機する cond.wait(lock, [&]{ return is_data_ready; });

// データが準備できたので処理開始 process_data();}

condition_variableの使用例

63

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

// Thread2void PrepareDataForProcessing() {

retrieve_data(); prepare_data(); // データを準備する

boost::lock_guard<boost::mutex> lock(mtx);

// データが準備できたら完了フラグをセットして is_data_ready = true;  // 待機状態のスレッドを起こす cond.notify_one();}

condition_variableの使用例

64

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

Producer/Consumerパターン

✤非同期処理のデザインパターンの一つ✤条件変数を利用して実装できる

65

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

66

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

キュー

データを追加する データを取り出す

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

67

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

キューが満杯だと

データを追加できない→ キューが空くまで待機

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

68

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4データが取り出されるとスペースが空く

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

69

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

空いたスペースにデータを追加

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

70

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

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

71

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

キューが空だと

データを取り出せない→ データが追加されるまで待機

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

72

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

データを追加するとキューが空でなくなる

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

73

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4追加されたデータを取り出す

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

74

Producer/Consumerパターン

Consumer1Producer1

Producer2

Producer3

Producer4

Producer5

Consumer2

Consumer3

Consumer4

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

template<class T>class LockedQueue { void enqueue(T val) { if(キューが満杯) { 待機; } // (1) キューの末尾にvalを追加; キューが空でなくなったら(2)に通知; } T dequeue() { if(キューが空) { 待機; } // (2) キューの先頭からデータを取り出し; 満杯だったキューが空いたら(1)に通知; }};

擬似コード

75

enqueue用/dequeue用にそれぞれ一つずつ条件変数を使う

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

std::promise/std::future

✤ Promiseパターンを実装したクラス✤あるタイミングで行った処理の結果を、別のタイミングで取得するための仕組み

✤マルチスレッドに限らず、非同期処理のための仕組み

76

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

// 値をセットするためのpromiseクラスを用意std::promise<int> p;

// promiseクラスのオブジェクトから、// 対応するfutureクラスのオブジェクトを作成std::future<int> f = p.get_future();

// pとfは"Shared State"という状態を共有しており、// これを通じてpからfへ値や例外を受け渡せる

std::promise/std::future

77

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

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

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

int main(){ foo(); // DoProcess(); std::cout << bar() << std::endl; //結果を取得}

スレッドを使用しない非同期処理

79

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

int main(){ std::thread th(foo); // 別スレッドで処理 std::cout << bar() << std::endl; //結果を取得

th.join();}

スレッドを使用して非同期処理

80

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

std::atomic

✤アトミック変数を実装したクラス✤アトミック変数へのアクセスは複数のスレッドから同時に行っても安全

81

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

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

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

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になる

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

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);

ロックしなくても安全

84

011A57B4  lock xadd   dword ptr [ecx],eax

(※コンパイラやコンパイルオプションによって生成されるアセンブラは異なるのでこれは一例)

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

std::atomic

✤アトミック変数によって、ロックを使用せずにマルチスレッドプログラムを記述できる

✤これを利用したアルゴリズムはロックフリーアルゴリズムと呼ばれる

✤詳しくは「プログラミングの魔導書 vol.3」参照

85

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

std::atomic使用上の注意

✤アトミック変数は、正しく使うのが難しいので注意✤ ハードウェアアーキテクチャのメモリモデルを理解していなければ、思わぬバグの原因となる

✤ 「リリースビルドだけでなぜか落ちる」

✤なので、同期処理に不安がある時はミューテックスでちゃんとロックを掛けて処理した方が良い

86

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

実践「task_queueクラス」

87

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

実践

✤ここまでに紹介した機能を使って、マルチスレッドをサポートしたタスクキュークラスを作成する

88

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

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;

task_queueクラス

89

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

90

task_queueの動作

タスクキューのスレッド1

ユーザー側のスレッド1

キュー

1.関数を追加 3. タスクを実行

タスクキューのスレッド2

タスクキューのスレッド3

タスクキューのスレッド4

ユーザー側のスレッド2

ユーザー側のスレッド3

ユーザー側のスレッド4

task_queueクラス

2.「タスク」として保持

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

//! タスクキューで扱うタスクを表すベースクラス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

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

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

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

// 続き

void process() { for( ; ; ) { // タスクが積まれていたら、 // キューから取り出す std::unique_ptr<Task> task = locked_queue_.dequeue();

task->run(); // F(Args...)を呼び出し // promiseに値を設定する } } // 各スレッドがprocess()を実行する std::vector<std::thread> threads_;};

93

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

実装したソース

✤今回の発表用のサンプルソースのtaskディレクトリ

✤機能が追加されているので、前述の実装イメージよりも複雑になっているが、やっていることはほぼ同じ

94

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

task_queueを使用

✤デモ(サンプルゲーム)✤複数のパーティクルの移動(計算負荷を高くしてある)にtask_queueクラスを使用し、並列処理をする✤ サンプルゲームは急ごしらえのため、ソースの品質についてはご容赦ください

95

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

まとめ

96

✤ C++に標準で用意されたクラスを利用してマルチスレッドプログラムを作成できる

✤マルチスレッドによって、パフォーマンスを向上できる

✤マルチスレッドプログラミングでは、データ競合/デッドロックに注意する(そのための仕組みも用意されている)

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

参考文献

✤ 「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