More Related Content
Similar to C++ マルチスレッドプログラミング
Similar to C++ マルチスレッドプログラミング (20)
More from Kohsuke Yuasa (9)
C++ マルチスレッドプログラミング
- 3. 発表用に用意したソース
✤ こちらに https://github.com/hotwatermorning/Ohotech10-SampleGame
✤ ライブラリのパスを設定してVisual Studio
2013でビルドすると、サンプルゲームがビルド
できる
3
- 7. void ThreadProcess1() {
doSomething1();
}
void ThreadProcess2() {
doSomething2();
}
int main() {
std::thread th1(ThreadProcess1);
std::thread th2(ThreadProcess2);
th1.join();
th2.join();
}
複数の実行の流れ
7
- 8. void ThreadProcess1() {
doSomething1();
}
void ThreadProcess2() {
doSomething2();
}
int main() {
std::thread th1(ThreadProcess1);
std::thread th2(ThreadProcess2);
th1.join();
th2.join();
}
複数の実行の流れ
8
- 13. スレッドによるパフォーマンス向上
✤ マルチコアCPUの余っているコアを使って処理を
行う
✤ パス探索/AI思考ルーチン
✤ マルチスレッド化の方針
✤ タスク並列
✤ 機能ごとに処理を分割し、マルチスレッドで実行する
✤ データ並列
✤ 単一の機能で処理するデータを分割し、マルチスレッドで実行する
✤ http://www.isus.jp/article/game-special/designing-ai-for-games-4/
13
- 14. マルチスレッドの難点
✤ スレッド間の同期処理/排他制御
✤ シングルスレッドのプログラミングよりも難くなる
✤ データ競合/デッドロック
✤ 思うようにパフォーマンスが向上しないことも
✤ 処理を分割する粒度、排他制御の仕方が悪ければ逆に
パフォーマンスを下げてしまう
14
- 15. 既存スレッドライブラリの利用
✤ マルチスレッドプログラミングは、複雑になりや
すく、並行処理にバグがあると発見が困難になる
✤ なので充分に検証されたより高級な仕組み(ライ
ブラリや言語拡張)が存在している場合は、それ
を使うほうが安心
✤ TBB, PPL, Parallel STL, OpenMP, ...
✤ ただし、このような仕組みを利用するには実行
ファイルの他にランタイムのDLLが必要になった
りすることがある
15
- 17. C++のスレッド
17
✤ (C++11という規格から)
標準規格にスレッドが定義され、
✤ スレッドを扱うクラスや、マルチスレッドプログ
ラミングを支援するクラスが用意された
- 18. 以前のC++
✤ 標準規格にスレッドが定義されていなかったの
で、C++の実装系やOSの定義をもとにマルチス
レッドプログラムを書く必要があった。
✤ 標準規格にライブラリも用意されていなかったの
で、OSが用意しているスレッドライブラリや
pthread, Boost.Threadなどを利用していた
✤ 現在でも、C++11準拠度の低いC++実装系を使用する場
合は、これらのライブラリを使用することになる
18
- 19. 標準規格に定義されたクラス
✤ std::thread
✤ std::mutex
✤ std::lock_guard/std::unique_lock
✤ std::condition_variable
✤ std::promise/std::future
✤ std::atomic
etc,...
19
- 22. // 別スレッドで呼び出したい関数
void ThreadProcess() {
std::cout << "Hello Thread World" << std::endl;
}
void foo() {
// スレッドを起動
std::thread th(ThreadProcess);
th.join();
}
スレッドの作成
22
- 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. // 関数呼び出し演算子を持つクラスのオブジェクト
// (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. スレッドを作成する
struct ClassX {
void Process() {}
};
ClassX x;
// メンバ関数のアドレスとオブジェクトを渡すと、
// これを結びつけて(bindして)
// スレッドを起動できる
std::thread th7(&ClassX::Process, x);
25
- 26. void DoSomething() { //時間のかかる関数
Sleep(1000/*millisec*/);
}
void foo() {
std::thread th(DoSomething); //スレッドを起動して
//スレッドの終了を待機する
th.join();
std::cout << "joined." << std::endl;
}
スレッドの終了を待機する
26
1秒後に`joined.`が出力される
- 27. スレッドを切り離す
void bar() {
std::thread th(DoSomething);
// スレッドを切り離し
th.detach();
std::cout << "detached." << std::endl;
} // thが破棄されてもスレッドは動き続ける
27
即座に`detached.`が出力される
- 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. 排他制御していないコード
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
詳しくは http://yohhoy.hatenablog.jp/entry/2013/12/15/204116
- 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
- 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. クリティカルセクション
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. クリティカルセクション
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
- 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. 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. 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の中途半端な状態
が観測されないようになる
- 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. 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
どちらのスレッドも
処理を進められなくなる
- 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. 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. boost::shared_mutex
50
✤ Reader/Writerロックを実現する
✤ ある変数を保護するための排他制御を行いたい
時、その変数へのアクセスのほとんどが読み込み
で書き込みが少ない時に使用する
✤ まだ標準規格には取り入れられていない
- 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. 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. 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. lock()/unlock()の問題点
int counter = 0;
std::mutex mtx;
void DoWork() {
DoSomething();
mtx.lock();
++counter; // ←ここで複雑なことをして例外が
// 発生したり、
mtx.unlock(); // ←ここでunlock()を忘れると、
// ロックが確保されたままになる
}
// この例だと、次回の呼び出しでデッドロックする
54
- 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. int counter = 0;
std::mutex mtx;
void DoWork() {
DoSomething();
// ロック対象の型をテンプレート引数に指定して
// コンストラクタに対象のオブジェクトを渡すと
lock_guard<std::mutex> lock(mtx);
++counter;
}
lock_guardを使用する
56
- 57. int counter = 0;
std::mutex mtx;
void DoWork() {
DoSomething();
// コンストラクタ内で自動的にmtxのロック確保
lock_guard<std::mutex> lock(mtx);
++counter;
// スコープを抜ける時にlock変数のデストラクタが
// 呼ばれ、ロックが解放される
}
lock_guardを使用する
57
- 58. int counter = 0;
std::mutex mtx;
void DoWork() {
DoSomething();
lock_guard<std::mutex> lock(mtx);
++counter;
}
lock_guardを使用する
58
Mutexのロックをオブジェクトの
寿命に紐付けて管理できる
- 59. std::lock_guard
59
✤ このロックの仕組みを実装したクラス
✤ テンプレート引数には、BasicLockable要件を満
たす型を指定できる
✤ つまり、std::mutexに限らずlock()/unlock()メンバ関数を
持つ型ならなんでもいい
✤ スコープ内でlock_guardクラスのオブジェクト
が生きている間、ロックを確保する
- 60. std::unique_lock
60
✤ std::lock_guardクラスの機能に加えて、より高
機能な仕組みをサポートする
✤ 明示的にロックを解放する
✤ ロックの所有権をムーブする
✤ ロックの確保を試行し、成功か失敗かを取得する
など
✤ スコープ内で単純なロックをかけたい場合は
lock_guardを
✤ ロックをより柔軟に管理したい場合は
unique_lockを使用するとよい
- 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. // Thread2
void PrepareDataForProcessing() {
retrieve_data();
prepare_data(); // データを準備する
boost::lock_guard<boost::mutex> lock(mtx);
// データが準備できたら完了フラグをセットして
is_data_ready = true;
// 待機状態のスレッドを起こす
cond.notify_one();
}
condition_variableの使用例
64
- 75. template<class T>
class LockedQueue {
void enqueue(T val) {
if(キューが満杯) { 待機; } // (1)
キューの末尾にvalを追加;
キューが空でなくなったら(2)に通知;
}
T dequeue() {
if(キューが空) { 待機; } // (2)
キューの先頭からデータを取り出し;
満杯だったキューが空いたら(1)に通知;
}
};
擬似コード
75
enqueue用/dequeue用に
それぞれ一つずつ条件変数を使う
- 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
- 80. int main()
{
std::thread th(foo); // 別スレッドで処理
std::cout << bar() << std::endl; //結果を取得
th.join();
}
スレッドを使用して非同期処理
80
- 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. ロックしなくても安全
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. ロックしなくても安全
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
- 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. 1.関数を追加3. タスクを実行
90
task_queueの動作
タスクキュー
のスレッド1
ユーザー側の
スレッド1
キュー
タスクキュー
のスレッド2
タスクキュー
のスレッド3
タスクキュー
のスレッド4
ユーザー側の
スレッド2
ユーザー側の
スレッド3
ユーザー側の
スレッド4
task_queueクラス
2.「タスク」
として保持
- 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. 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. // 続き
void process() {
for( ; ; ) {
// タスクが積まれていたら、
// キューから取り出す
std::unique_ptr<Task> task =
locked_queue_.dequeue();
task->run(); // F(Args...)を呼び出し
// promiseに値を設定する
}
}
// 各スレッドがprocess()を実行する
std::vector<std::thread> threads_;
};
93
- 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