そろそろvolatileについて一言いっておくか

Slides:



Advertisements
Similar presentations
シーケンス図の生成のための実行履歴圧縮手法
Advertisements

【講座3】 MP T-Kernel入門 (株)日立超LSIシステムズ 豊山 祐一
メモリコンシステンシモデル memory consistency model
プログラミング基礎I(再) 山元進.
スレッドの同期と、スレッドの使用例 スレッドの同期 Lockオブジェクト: lockオブジェクトの生成
Javaのための暗黙的に型定義される構造体
実行時のメモリ構造(1) Jasminの基礎とフレーム内動作
C言語 配列 2016年 吉田研究室.
第2回:Javaの変数と型の宣言 プログラミングII 2007年10月2日.
侵入検知システム(IDS) 停止 IDS サーバへの不正アクセスが増加している
アルゴリズムとデータ構造 2011年6月13日
構造体.
条件式 (Conditional Expressions)
プログラミング演習II 2004年12月 21日(第8回) 理学部数学科・木村巌.
1.12 式における型変換 1.13 代入における型変換 1.14 コメント 10月31日(金) 発表者:藤井丈明
プログラミング演習Ⅰ 課題2 10進数と2進数 2回目.
ML 演習 第 7 回 新井淳也、中村宇佑、前田俊行 2011/05/31.
CONCURRENT PROGRAMMING
第6回独習Javaゼミ 第6章 セクション4~6 発表者 直江 宗紀.
15.同時実行制御,トランザクション, データベースの回復
型付きアセンブリ言語を用いた安全なカーネル拡張
細かい粒度でコードの再利用を可能とするメソッド内メソッドのJava言語への導入
プログラミング論 関数ポインタ と 応用(qsort)
プログラミング言語入門 手続き型言語としてのJava
プログラミング2 関数
細かい粒度で コードの再利用を可能とする メソッド内メソッドと その効率の良い実装方法の提案
独習JAVA 6.8 コンストラクタの修飾子 6.9 メソッドの修飾子 6.10 ObjectクラスとClassクラス 11月28日(金)
C言語でスレッド (Pthread) 2007年1月11日 海谷 治彦.
暗黙的に型付けされる構造体の Java言語への導入
最適化の方法 中田育男著 コンパイラの構成と最適化 朝倉書店, 1999年 第11章.
プログラミング入門 電卓を作ろう・パートIV!!.
コンピュータ系実験Ⅲ 「ワンチップマイコンの応用」 第1週目 アセンブリ言語講座
アルゴリズムとプログラミング (Algorithms and Programming)
アルゴリズムとデータ構造 補足資料11-1 「mallocとfree」
依存型で型付けされた 副作用のある関数のための 型保存コンパイラ
Talkプログラムのヒント 1 CS-B3 ネットワークプログラミング  &情報科学科実験I.
計算機構成 第3回 データパス:計算をするところ テキスト14‐19、29‐35
計算機工学III オペレーティングシステム #4 並行プロセス:排他制御基礎 2006/04/28 津邑 公暁
第4回 ファイル入出力方法.
フロントエンドとバックエンドのインターフェース
岩村雅一 知能情報工学演習I 第12回(C言語第6回) 岩村雅一
復習 一定回数を繰り返す反復処理の考え方 「ループ」と呼ぶ false i < 3 true i をループ変数あるいはカウンタと呼ぶ
C言語ファミリー C# 高級言語(抽象的) Java オブジェクト指向 C++ C 機械語(原始的)
アルゴリズムとプログラミング (Algorithms and Programming)
参照されないリテラル 長谷川啓
同期処理のモジュール化を 可能にする アスペクト指向言語
計算機プログラミングI 木曜日 1時限・5時限 担当: 増原英彦 第1回 2002年10月10日(木)
アルゴリズムとデータ構造 2012年6月11日
プログラミング論 ポインタ
アルゴリズムとデータ構造1 2009年6月15日
オペレーティングシステムJ/K (並行プロセスと並行プログラミング)
復習 if ~ 選択制御文(条件分岐) カッコが必要 true 条件 false 真(true)なら この中が aを2倍する 実行される
4.プッシュダウンオートマトンと 文脈自由文法の等価性
SMP/マルチコアに対応した 型付きアセンブリ言語
オブジェクト指向言語論 第二回 知能情報学部 新田直也.
アルゴリズムとデータ構造 2010年6月17日
プログラミング入門2 第6回 関数 情報工学科 篠埜 功.
プログラミング言語論 第九回 理工学部 情報システム工学科 新田直也.
Javaとは Javaとはオブジェクト指向言語でJava VM(Java仮想マシン)と呼ばれるプログラム上で動作します。
プログラミング演習I 2003年6月11日(第9回) 木村巌.
情報処理Ⅱ 第2回 2004年10月12日(火).
オブジェクト指向言語論 第七回 知能情報学部 新田直也.
プログラミング言語論 第九回 理工学部 情報システム工学科 新田直也.
オブジェクト指向言語論 第七回 知能情報学部 新田直也.
情報処理Ⅱ 2005年11月25日(金).
情報処理Ⅱ 小テスト 2005年2月1日(火).
復習 いろいろな変数型(2) char 1バイト → 英数字1文字を入れるのにぴったり アスキーコード → 付録 int
オブジェクト指向言語論 第六回 知能情報学部 新田直也.
計算機プログラミングI 第2回 2002年10月17日(木) 履習登録 複習 ライブラリの利用 (2.6-7) 式・値・代入 (2.6-8)
計算機プログラミングI 第5回 2002年11月7日(木) 配列: 沢山のデータをまとめたデータ どんなものか どうやって使うのか
Presentation transcript:

そろそろvolatileについて一言いっておくか

自己紹介 H/N: yamasa FreeBSD使い Java屋 モバイル大好き

今日のキーワード アトミック変数 メモリバリア happens before 正しく同期化されたコード volatile

マルチスレッドプログラミングで苦労する点 マルチスレッドと排他制御 マルチスレッドプログラミングで苦労する点 同じ変数やオブジェクトが複数のスレッドから 同時にアクセスされる可能性がある。 mutex などの排他制御(ロック)を使って対処 あらゆる変数を排他制御しなければならないの?ロックによるパフォーマンスの劣化も気になる。 ⇒ もっと軽量の仕組みはないの?

アトミック変数とメモリバリア スレッド間でデータをやり取りするための最も基本的な仕組み(primitive) それが… volatile

複数スレッドが同時に読み書きなどの操作を行っても、それらの操作が順々に行われているように見えることが保証されている変数 アトミック変数とは 「アトミック変数」とは 複数スレッドが同時に読み書きなどの操作を行っても、それらの操作が順々に行われているように見えることが保証されている変数

アトミック変数へのloadとstore (1) // 初期値 atomic<int> a = -1 スレッド1: スレッド2: // aに1を代入(store) // aの値を読み出す(load) a.store(1) r = a.load() r の値は -1 または 1 のどちらかになることが保証される

アトミック変数へのloadとstore (2) // 初期値 atomic<int> a = 0 スレッド1: スレッド2: a.store(1) a.store(-1) a の値は 0 ⇒ 1 ⇒ -1 0 ⇒ -1 ⇒ 1 のいずれかの順序で変化することが保証される

load, storeのアトミック性 当たり前のようだが非常に大切な性質 アトミック性が保証されない変数に対して同様にアクセスした場合、たとえば上位bitと下位bitが混ざった値になってしまうかも… (例: 32bitアーキテクチャでの int64_t (long long)型変数への読み書き)

多くのアトミック変数の実装には、読み込みと書き込みを同時に行う操作も提供されている アトミック変数への複雑な操作 多くのアトミック変数の実装には、読み込みと書き込みを同時に行う操作も提供されている r1 = a.exchange(r2) ⇒ { r1 = a; a = r2; } // 読み込みと同時に書き込み r1 = a.fetch_add(r2) ⇒ { r1 = a; a = a + r2; } // 読み込みと同時に加算 など…

アトミック変数への書き込みは、いずれ必ず他スレッドからも見えるようになる。 アトミック変数のもう一つの性質 アトミック変数への書き込みは、いずれ必ず他スレッドからも見えるようになる。 atomic<int> a = 0; スレッド1: スレッド2: a.store(1); while (a.load() != 1) { } この場合、スレッド2のwhile文は無限ループにならないことが保証される。

アトミック変数は、複数のスレッドからアクセスされても大丈夫なように設計された変数 アトミック変数と普通の変数の違い アトミック変数は、複数のスレッドからアクセスされても大丈夫なように設計された変数 ⇒ アトミックではない普通の変数は、マルチスレッドでは予期せぬ実行結果になることがある。 マルチスレッドプログラムでは全ての変数をアトミック変数にしなければならないの? ⇒ No! 普通の変数についても、マルチスレッドでの動作を保証する仕組みが別に存在する!

以後の説明では変数を以下のように 表記します a, a1, a2 … アトミック変数 x, y, z スレッド間でアクセスされる可能性がある アトミック変数以外の普通の変数 r, r1, r2 … ローカル変数(レジスタ) また、明示的に示さない場合の各変数の初期値は0とします

プログラム実行速度の向上などの目的で実行順序を入れ替える、最適化手法の一つ リオーダーとは? 「リオーダー」 プログラム実行速度の向上などの目的で実行順序を入れ替える、最適化手法の一つ コンパイラがプログラムコードを機械語に変換するとき CPUが機械語のコードを実際に実行するとき キャッシュメモリの影響などを考慮して命令の実行順序を入れ替える

当然ながら、実行結果が変わってしまうようなリオーダーは禁止 リオーダーが出来ない状況 当然ながら、実行結果が変わってしまうようなリオーダーは禁止 x = 1; // …① y = x; // …② ⇒ ①と②を入れ替えると y に代入される値が変わってしまうのでダメ! ⇒でも、②の右辺を定数1に置換する最適化はOK

①と②´、③と④をそれぞれ入れ替えるのはOK リオーダーできる例 では、以下の例はリオーダー可能? // 例1 x = 1; // …① y = 1; // …②´ // 例2 r1 = y; // …③ r2 = x; // …④ 最終的な実行結果は変わらないので、 ①と②´、③と④をそれぞれ入れ替えるのはOK

r1 == 1 && r2 ==0 となることはありえない、 …はず。 でも、リオーダーを考慮すると、ありえる。 …ただし、シングルスレッドに限る 前述の例をマルチスレッド化すると… // スレッド1 // スレッド2 x = 1; // …① r1 = y; // …③ y = 1; // …②´ r2 = x; // …④ r1 == 1 && r2 ==0 となることはありえない、 …はず。 でも、リオーダーを考慮すると、ありえる。

マルチスレッドでは、ちょっとした最適化でも実行結果に影響を及ぼす。 マルチスレッドでの最適化の困難さ マルチスレッドでは、ちょっとした最適化でも実行結果に影響を及ぼす。 ⇒ マルチスレッドでの実行結果にも影響しないようにリオーダーなどを制限すると、最適化する余地がほとんど無くなってしまう!!

よって、最適化の規則を以下のように定める。 シングルスレッドでの実行結果が変わらない限り、どんな最適化も基本的には許可する。 マルチスレッドでの最適化の原則 よって、最適化の規則を以下のように定める。 シングルスレッドでの実行結果が変わらない限り、どんな最適化も基本的には許可する。 最適化がマルチスレッドでの動作に悪影響を及ぼさないよう、最適化を制限する手段を処理系は提供する。 ⇒ それが「メモリバリア(メモリフェンス)」

メモリバリアの基本は2種類 releaseバリア acquireバリア releaseバリア acquireバリア 命令① 命令① release_barrier(); acquire_barrier(); 命令② 命令② releaseバリア 先行する命令が、バリアを超えて後ろにリオーダーされるのを禁止する。 acquireバリア 後続の命令が、バリアを超えて前にリオーダーされるのを禁止する。 OK OK

アトミック変数とメモリバリアの組み合わせ アトミック変数とメモリバリアは一緒に用いる アトミック変数へのstore + releaseバリア 処理① a.store_release(r); アトミック変数からのload + acquireバリア r = a.load_acquire(); 処理②

アトミック変数とメモリバリアを組み合わせることで、異なるスレッドの命令間に順序付けをすることができる。 atomic<int> a = 0; int x = 0; スレッド1: スレッド2: x = 1; // …① a.store_release(1); r1 = a.load_acquire(); if (r1 == 1) { r2 = x; // …② }

store_release()で書き込んだ値をload_acquire()で読み込むことで、 2つの間に前後関係が生まれる。 スレッド1: スレッド2: x = 1; // …① a.store_release(1); r1 = a.load_acquire(); if (r1 == 1) { r2 = x; // …② } store_release()で書き込んだ値をload_acquire()で読み込むことで、 2つの間に前後関係が生まれる。 この関係を “synchronize with” と呼ぶ。 synchronize with

releaseバリアのため、①の書き込みは必ず store_release() より前に行われる。 スレッド1: スレッド2: x = 1; // …① a.store_release(1); r1 = a.load_acquire(); if (r1 == 1) { r2 = x; // …② } releaseバリアのため、①の書き込みは必ず store_release() より前に行われる。 acquireバリアのため、②の読み込みは必ず load_acquire() の後に行われる。 (要するに、xの値の先読みは禁止!) s.w.

synchronize with関係とメモリバリアの効果により、①と②の間に保証された順序関係が生じる。 スレッド1: スレッド2: x = 1; // …① a.store_release(1); r1 = a.load_acquire(); if (r1 == 1) { r2 = x; // …② } synchronize with関係とメモリバリアの効果により、①と②の間に保証された順序関係が生じる。 この関係(“happens before” と呼ぶ)が存在することにより r2 == 1 が保証される。 happens before

推移的なhappens before関係(1) スレッド1: スレッド2: スレッド3: x = 1; // …① a1.store_release(1); r1 = a1.load_acquire(); if (r1 == 1) { r = x; // …② a2.store_release(1); r2 = a2.load_acquire(); } if (r2 == 1) { x = 2; // …③ } ①の書き込みは②より前だが、③の書き込みは②より後。 ⇒ よって r == 1 となる。 h.b. h.b.

推移的なhappens before関係(2) スレッド1: スレッド2: スレッド3: x = 1; // …① a1.store_release(1); r1 = a1.load_acquire(); if (r1 == 1) { x = 2; // …② a2.store_release(1); r3 = a2.load_acquire(); } if (r3 == 1) { r = x; // …③ } ①で書き込まれた値は、その後の②で書き込まれた値によって上書きされる。 ⇒ よって r == 2 となる。 h.b. h.b.

happens before関係がない場合(1) スレッド1: スレッド2: スレッド3: x = 1; // …① a.store_release(1); r1 = a.load_acquire(); r3 = a.load_acquire(); if (r1 == 1) { if (r3 == 1) { r2 = x; // …② r4 = x; // …③ } } ②も③も、①より後なので、 r2 == 1 かつ r4 == 1 となる。 ②と③の間には happens before の関係が無いが、問題ない。 h.b. h.b. ?

happens before関係がない場合(2) スレッド1: スレッド2: スレッド3: x = 1; // …① x = 2 // …② a1.store_release(1); a2.store_release(1); r1 = a1.load_acquire(); r2 = a2.load_acquire(); if (r1 == 1 && r2 == 1) { r = x; // …③ } ①と②の間には happens before の関係がない。 ⇒このような状態を “data race” と呼ぶ。 ?

happens before関係がない場合(3) スレッド1: スレッド2: x = 1; // …① a.store_release(1); r1 = a.load_acquire(); x = 2; // …③ if (r1 == 1) { r = x; // …② } ②と③の間には happens before の関係がない。 ⇒これも “data race” である。 h.b. ?

アトミック変数ではない普通の変数に対して、異なるスレッド上からの data raceとは アトミック変数ではない普通の変数に対して、異なるスレッド上からの 書き込みと書き込み または 書き込みと読み込み があり、2つの間に happens before の関係がない場合を “data race” と呼ぶ。

要するに、「data raceが起きた時点で負け」 Javaの場合 「あらゆる最適化を考慮した上で起こり得る、全ての結果候補のうちのいずれか」 C++の場合 undefined (未定義)。「何が起こるかわからない」 要するに、「data raceが起きた時点で負け」 data race が起きないプログラムコードのことを、「正しく同期化されている」と呼ぶ

正しく同期化されたコードを書くための三か条 正しく同期化されたコードを書くには 正しく同期化されたコードを書くための三か条 変更したデータを、他スレッドからもアクセスできるようにするときは、releaseバリアを発行する。 他スレッドが変更したデータへアクセスするときには、acquireバリアを発行する。 あるデータを他スレッドが読み書きしているタイミングでは、そのデータへの書き込みを行わないようにする。

正しく同期化されているので、Hogeオブジェクトへはスレッド2から安全にアクセスできる。 正しく同期化されたコードの例(1) Hoge* x; atomic<bool> a = false; スレッド1: スレッド2: x = new Hoge(); a.store_release(true); while (!a.load_acquire()) { } x->foo(); 正しく同期化されているので、Hogeオブジェクトへはスレッド2から安全にアクセスできる。 h.b.

正しく同期化されているので、Hogeオブジェクトへはスレッド2から安全にアクセスできる。 正しく同期化されたコードの例(2) atomic<Hoge*> a = NULL; スレッド1: スレッド2: Hoge* r1 = new Hoge(); Hoge* r2; a.store_release(r1); do { r2 = a.load_acquire(); } while (r2 == NULL); r2->foo(); 正しく同期化されているので、Hogeオブジェクトへはスレッド2から安全にアクセスできる。 h.b.

exchangeは、書き込むと同時にその直前の値を返す操作 スピンロックへの応用 アトミック変数とメモリバリアを用いると、 いわゆる「スピンロック」も実現できる。 atomic<bool> a = false; void spin_lock() { while(a.exchange_acquire(true)) { } } void spin_unlock() { a.store_release(false); exchangeは、書き込むと同時にその直前の値を返す操作 それぞれ acquire, release のメモリバリアを持っていることに注目

スピンロックのもつメモリバリア効果によりhappens before関係が生まれるので、変数を安全に共有できる。 スピンロックによる同期化 スレッド1: スレッド2: spin_lock(); x = 1; spin_unlock(); spin_lock(); r = x; spin_unlock(); スピンロックのもつメモリバリア効果によりhappens before関係が生まれるので、変数を安全に共有できる。 happens before synchronize with

スピンロックに限らず、スレッド間の排他制御や同期化を行う仕組み(mutexやセマフォ等)は、メモリバリア効果も持っている。 排他制御とメモリバリア スピンロックに限らず、スレッド間の排他制御や同期化を行う仕組み(mutexやセマフォ等)は、メモリバリア効果も持っている。 ⇒ これらの仕組みを正しく使うことでも、安全なデータ共有を行うことが可能。 “acquireバリア”, “releaseバリア” という名称も、ロック取得と開放のもつメモリバリア効果に対応して名付けられている。

最適化の影響を考えずにマルチスレッドなプログラムを書くと、予期しない実行結果になることがある。 ここまでのまとめ 最適化の影響を考えずにマルチスレッドなプログラムを書くと、予期しない実行結果になることがある。 アトミック変数とメモリバリアを使って正しく同期化されたコードを書けば、マルチスレッドでも安全にデータ共有を行うことができる。 ロックなどの同期化機構も、アトミック変数とメモリバリアで構築されている。

ここまでアトミック変数とメモリバリアについて説明してきました。 volatileの出番は? ここまでアトミック変数とメモリバリアについて説明してきました。 「ところで、 volatile変数ってアトミック変数やメモリバリアの代わりにならないの?」 ⇒ 答: C/C++においては「ならない」

volatile変数はアトミック性が保証されない ⇒ ++ や += などの演算子はもちろん、単なるloadやstoreも型によってはアトミックとならない。 volatile long long v; v = 5; movl $5, v movl $0, v+4 64bit変数への代入が2命令に分かれてしまう。 x86用gccでコンパイル

volatile変数はメモリバリア効果を持たない volatile int v; int x, y; x = 1; r1 = x; v = 2; r2 = v; y = 3; r3 = y; volatile変数をまたいだリオーダーが自由に起こる

一応、volatile変数同士では、コンパイラによるリオーダーはされないが… volatile int v1, v2; v1 = 1; r1 = v1; v2 = 2; r2 = v2; これでは、スレッド間で共有される全ての変数にvolatileを付けなければならなくなる!

しかも、CPUによってもリオーダーされることがあるアーキテクチャでは、機械語レベルでも「メモリバリア命令」が必要となる。 volatileの問題点その3 しかも、CPUによってもリオーダーされることがあるアーキテクチャでは、機械語レベルでも「メモリバリア命令」が必要となる。 ⇒ が、コンパイラがvolatile変数の読み書きに対してメモリバリア命令を発行してくれる保証も無い。

⇒ NG!! 要するに、C/C++のvolatileはマルチスレッドのことを何も考えていない!

次期C++標準である C++0x (aka C++201x) では、アトミック変数やメモリバリアの仕様が登場する。 そしてatomicへ… 次期C++標準である C++0x (aka C++201x) では、アトミック変数やメモリバリアの仕様が登場する。 volatileに関する定義はそのままで、アトミック型を別途定義している。 namespace std { template<class T> struct atomic; }

C++0xのatomic型では、メモリバリアの有無を引数で指定する。 void store(T, memory_order = memory_order_seq_cst); T load(memory_order = memory_order_seq_cst); デフォルト値は「メモリバリアあり」。 「ゼロオーバーヘッド原則」のC++がコストのかかるメモリバリア付きをデフォルトとすることからも、アトミック変数とメモリバリアを一緒に扱うことの重要性がわかる。

一方、Javaのvolatile変数は、load, storeのアトミック性やメモリバリア効果を備えている。 つまり、C++0xのatomic型に近い使い方ができる。 ただし、 ++ や += 演算子はアトミックではないので、これらのアトミック演算が必要であれば java.util.concurrent.atomic.Atomic* を使う。 C/C++とJavaでは、volatileの意味が全く変わってしまっている。

スレッド間通信のprimitiveであるアトミック変数とメモリバリアについて。 まとめ スレッド間通信のprimitiveであるアトミック変数とメモリバリアについて。 happens before関係の説明と、正しく同期化されたコードの作り方。 C/C++のvolatileは、マルチスレッドでは役立たず。 C++0xとJavaにおけるアトミック変数、メモリバリア。

他の言語では、アトミック変数やメモリバリアに相当する機能はどう扱われているの? ところで… 他の言語では、アトミック変数やメモリバリアに相当する機能はどう扱われているの? ⇒ 皆さんへの宿題としますので、わかったらLTなどで発表してみてください。(笑)