マルチスレッド時のメモリのシンクロ保証イロイロ
PROCESS WARPでは、今マルチスレッドプログラムの分散処理機能を作っています。同機能の実現には複数デバイスでメモリを同調、シンクロさせる必要があります。PROCESS WARPはMap Reduceや関数型言語でない普通のプログラムを実行するVMを作ろうとしているため、この点が複雑になります。 メモリのシンクロのタイミングは分散処理のナイーブな部分です。 メモリのシンクロ部分の設計にあたり、C/C++のメモリのシンクロがどのようになっているか調べました。
スレッド間で授受する値はatomic(原子)操作を行う
以下の様な単一の命令であっても、実はaの値は不定のタイミングが存在する可能性があり、C/C++はソレを許容しています。
a ++; // 次の処理〜
普通のシングルスレッドのプログラムの場合は次の処理が行われるまでに変数aの値が確定するので問題になりませんが、マルチスレッドの場合、変更途中のaの値を取得すると変更前の値とも変更後の値とも異なる値が取得される場合があります。 そんな事にならないように安全に値を書き換える、値を取得するのがatomic操作です。
たとえ話
- ホワイトボードに数字を書く係(A)の人がいます
- ソレを見て記録する係(B)の人がいます
- 一人で両方の仕事をしている場合は、問題は発生しません(シングルスレッド)
- 二人で仕事を手分けした場合、Aが10とホワイトボードに書いている途中にBが1と記録するかも知れませんし、Aが右から01と書くかもしれません
- なので、Aが最後にピリオドを打つまでを1つの作業とし、それを確認した段階でBが記録を行うようにしました(atomic)
スレッド間で値の操作に順序性を持たせる場合はメモリバリア操作を行う
C/C++コンパイラやCPUの命令デコーダはシングルスレッドで問題にならない範囲で最適化のために命令実行順序を変更します。変数書き換えがアトミックだとしても以下のコードを実行した場合、aとb、CPUがどちらを先に書き換えるかは分かりません。条件分岐など、変数を利用する処理が実行される前までに書き換えられます。変数cに至っては条件分岐に利用されないため、if文実行時の値が0のままの可能性もあります。
// a = 0; b = 0; c = 0; a = 1; b = 1; c = 1; if (a < b) // 条件分岐
値の書き換え順序を、「ここまでには変数aを書き換えておく」「変数bの書き換えはここ以降で行う」のように保証するのがメモリバリアです。
スレッド間で同時実行数の制御を行うにはmutex(ミューテクス or ミューテックス)、semaphore(セマフォ)を使う
メモリバリアは数値の書き換えに限定していましたが、一連の処理(クリティカルセクション)を同時に実行しないよう保証する機能があります。 セマフォは、処理を同時にn個のスレッドしかできないようにするという仕組みです。 特に、同時に1つのスレッドしか処理しない場合をミューテクスと呼びます。
梅酒の経過報告1
前回の続きです。あんずが手に入ったので早いですがウイスキーを抜いて、あんずと氷砂糖を入れました。
- もう少し梅をお酒につけておきたいと思った場合、ウイスキーを抜かずにあんずを入れればOKです(氷砂糖は入れず、あとからウイスキーを抜いて入れる)。
- 手に入ったあんずもウメと同じ要領で判別してヘタを取ります(アク抜きでないので10分ほど水に浸すだけでOK)。
- ウイスキーを抜きます(自分はもともとウイスキーが入っていたボトルに戻しています)。
- 梅の色が黄金色になっています(てっぺんの橙色の実はあんずです)。
- 氷砂糖は300gと500gを入れました。前回の400gはくらいと書いたので分量通りです。
- 梅とあんずが乾燥しないように瓶を振る日々が始まります。
pthreadとスレッド終了時の動作について
PROCESS WARPでマルチスレッド対応、分散処理対応を行うためにpthreadの動作、特に終了処理について調査しました。 manを読みながら明記されていない部分をMacOSX環境でサンプルを作って確認しました。
- exitで終了した場合、子スレッドも終了される。
- atexitで登録した関数が終了前に呼び出される
- 子スレッドでatexitで関数を登録しても、プロセス終了時に親スレッドで実行される
- 子スレッドでexitを実行すると同一プロセスの全てのスレッドが終了する
- pthread_clean_pushで登録した関数はexit時は実行されない
- pthread_key_createで登録したデストラクタはexit時は実行されない
- 子スレッドだけ終了するにはpthread_exitを利用する。
- pthread_clean_pushで登録した関数がpthread_exitで実行される
- pthread_key_createで登録したデストラクタが実行される
- 関数が実行されるのはpthread_exitが呼び出されたか処理が終了したタイミングであり、joinのタイミングではない
- 大元のスレッドでもpthread_exitを呼び出せる
- pthread_clean_push、pthread_key_createで登録した関数が実行される
- その後atexitで登録した関数が実行される
- pthread_exit以降の処理は実行されない
- 正常終了扱いになる
- スレッドがmutexを保持したままpthread_exitやreturnで終了した場合、mutexはロック状態のままになる
- 他のスレッドからthread_mutex_unlockを呼び出せばロックは解除できる
- pthread_key_createで登録したデストラクタはpthread_setspecificでNULL以外を指定しているプロパティに対して呼び出される
- returnでスレッド終了しても呼び出される
- 大元のスレッドではreturn終了では呼び出されない
なので、
- atexitで登録した関数のスタックはプロセスで1つだけ
- pthread_key_createで登録したキー、デストラクタはプロセス内のスレッドで共通
- pthread_setspecificで登録した値はスレッドごとに保持される
- pthread_clean_pushで登録した関数はスレッドごとに保持される(スタックに積まれる)
- exitで終了時または大元のスレッドがreturnする場合は、スレッドごとの終了処理はされない
- pthread_exit呼び出し時はpthread_clean_push, pthread_key_createで登録した関数の順に呼び出す
- pthread_exitを呼び出したのが大元のスレッドの場合は、その後にプロセスの正常終了処理を行う
という作りになっているようです。
C/C++のAPIの規格
標準Cライブラリ、POSIXなどの単語は知っているものの、厳密な規格は分かっていなかったので調べました。
標準Cライブラリ
Cの規格はISO/IEC 9899で定義されているそうです。APIについても同時に定義されているようです。
俗称 | ISO | JIS |
---|---|---|
C89? C90 | ISO/IEC 9899:1990 | JIS X 3010-1993 |
C95 | ISO/IEC 9899/AMD1:1995 | JIS X 3010:1993/AMENDMENT 1:1996 |
C99 | ISO/IEC 9899:1999 | JIS X 3010:2003 |
C11 | ISO/IEC 9899:2011 |
標準C++ライブラリ
C++の規格はISO/IEC 14882で定義されてるそうです。
俗称 | 互換のあるCの規格 | ISO | JIS |
---|---|---|---|
C++98 | C90 | ISO/IEC 14882:1998 | |
C++03 | C95 | ISO/IEC 14882:2003 | JIS X 3014:2003 |
C++11 | C99 | ISO/IEC 14882:2011 | |
C++14 | C99? | ISO/IEC 14882:2014 |
POSIX (Portable Operating System Interface for uniX) / SUS (Single UNIX Specification)
POSIXやSUSはAPIの提示だけでなく、ファイル構成やコマンドも含んでいます。 表の前半部分は多分UNIX戦争の影響なのでしょうか。
マーク | ベースとなるPOSIX | POSIXの規格 | SUSのバージョン |
---|---|---|---|
UNIX 93 | |||
UNIX 95 | |||
UNIX 98 | SUSv2 | ||
UNIX 03 | POSIX:2001 | IEEE Std 1003.1-2001 | SUSv3 |
POSIX:2004 | IEEE Std 1003.1-2004 | ||
POSIX:2008 | IEEE Std 1003.1-2008 | SUSv4 |
WikipediaではANSI/ISO C ⊂ POSIX.1 ⊂ SUSの関係になるらしいけど。。説明を読む感じだとどうなんでしょうね?
C/C++コンパイラでの標準ヘッダファイルのパスの指定方法方
PROCESS WARP用のプログラムコンパイル環境を作るにあたり/usr/includeのなどの大元のヘッダファイルの場所の変更方法を調べました。
clang -I <某>
と指定すると既存の/usr/include(など)に追加してヘッダの場所を指定することができることは知っていました。
何もオプションを指定しなくとも/usr/includeへのパスが通っています。
PROCESS WARPは全環境で同じAPI定義(ABI)を使うため、環境依存が生じる既存の/usr/includeは利用しないための方法を調べました。
調べた結果、 環境変数C_INCLUDE_PATH, CPLUS_INCLUDE_PATHを指定すると大元のヘッダファイルの場所のを変更できました。 複数のパスを指定する場合はコロンで区切ります。
emscriptenでC/C++プログラムをwebブラウザから使うまでの難所攻略
CMU#29で「emscriptenでC/C++プログラムをwebブラウザから使うまでの難所攻略」というタイトルで発表してきました。発表資料の作成など、振り返りにもなりました。
www.slideshare.net
伊藤式ウイスキー梅酒の作り方
小さい頃から祖母が梅ジュースを作るのを普通に見ており、社会人になってから自分は梅酒をつけるようになりました。毎年改良を加えながら作っており、ウイスキー梅酒はとくに好評なので作り方を載せておきます。是非ご家庭で梅酒をつけてみましょう!
前提
- お酒は20歳になってから
材料
- 梅(玉が大きい物) 1Kgくらい
- ウイスキー 1.8〜2Lくらい
- 氷砂糖 400gくらい
- あんず 1個(1ヶ月後くらいに手に入れば)
道具
- 梅酒用のビン4Lサイズ以上
- 竹串
作り方
- ビンをよく洗い、完全に乾かしておきます。モノによっては熱湯消毒NGなので、取説に従いましょう。
- 青梅の場合、1時間位水につけてアク抜きをします。
- 水分を拭き取りながら、ヘタの部分をほじり出し、傷がついた梅を除外します。傷がついていてもカサブタになっているものは大丈夫です。目安は写真の通り、水につけた後に変色しているものは除外します。
- ビンの中に梅、ウイスキーを静かに入れます。
- 蓋をしっかり閉め、1ヶ月くらい冷暗所に置いて待ちます。梅がウイスキーを吸ってパンパンになります。
- 1ヶ月くらいしたら、ウイスキーを別のビン(もともとウイスキーが入っていたものなど)に移します。ウイスキーがちょっとくらい梅のビンに残っていてもOKです。
- あんずが手に入った場合、梅と同じように洗い、ヘタを取り、水分を拭き取ってから一緒に入れます。
- 氷砂糖を梅の上からふりかけ軽く振ります。
- 蓋をしっかり閉め、冷暗所に置き、さらに1ヶ月くらい待ちます。梅が空気に触れている場合、2・3日おきにビンを揺らし乾かないようにします。乾くとカビてしまい失敗します。梅のエキスとアルコールが搾り取られシワシワになります。氷砂糖が残っていたり梅が浮いている場合はもうチョット置きましょう。
- ウイスキーを梅のビンに戻します。蓋をしっかり閉め、冷暗所に置きます。梅は浮かないはずですが、浮いている場合は2・3日おきにビンを揺らして梅が乾かないようにし、沈むのを待ちます。
- 3ヶ月位すると飲めるようになってきますが、半年以上置くことをおすすめします。
飲み方
- ストレート、ロック、ソーダ割りは普通に美味しいです。
- お湯で割ると香りが特に良いです。
- トニックは相性が難しいようで、オススメの物があったら教えて下さい。
- 度数が高いアルコールを使った場合、冷凍庫でキンキンに冷やすこともできますが、香りが、、、
その他
- 氷砂糖は普通の作り方より減らしても十分梅のエキスを搾り取ることができます。どこまで減らせるかはまだ試していないので分かっていません。
- 梅を取り出すかどうかについては好みでどうぞ。自分は飲み始めてお酒から梅が出るようになったらカビが怖いので全部取り出してしまいますが、ソレ以外は放置です。
- ウイスキーの銘柄も好みだと思います。自分はキリンの富士山麓を使っています。