シンクロってなんだかなぁ

某MLで延々と議論が続いているので,ウォッチしています。

妄想も交えてメモメモ。

はじめに:
まず,
 ・マルチスレッド
 ・マルチプロセス
 ・マルチプロセッサ
をわけて考えることをオススメします。マイコン系やご老人なら,
 ・マルチタスク
もあるでしょう。あるいは,
 ・メインルーチンと割り込みルーチン
という分け方も現存します。
そしてさらに,
 ・プロセス
 ・スレッド
のOS毎の実装について考慮しておきましょう。UNIX系ですと,スレッドの実装にも歴史的経緯が絡んで厄介な話が満載です。

Windowsでは,ファイバのことはこの際忘れましょう。後で考えれば,たいしたことではありません。ついでにマイクロスレッドや,ライトウェイトスレッドについても忘れておきましょう。Apacheのスレッドプールとかも忘れてください。お願いします;;

 ・ジョブ
なんて話は最初から考えるつもりもありません。JCLのマニュアルに排他のことがちゃんと書いてあるらしいですから読んでくだせ〜m(__)m

ついでに,DSPな人も読まないで下さい。特にハーバードアーキテクチャな人。最近はもっと恐ろしいバス構成だそうですね。同情いたします。きっと素晴らしいコンパイラをお使いのはずですから,そちらのマニュアルを読んでください。ちゃんと書いてあるはずです。

ここまで書くと,この手の議論がごく狭い問題領域しか取り扱っていないことに気づかされます。そうです。問題領域としては狭いのです。そしてハマリング人口も微々たる物です。しかしながらその僅かな人々のせいで,フリーズするアプリに多くの人が悩まされることになっているのが現状です。

「そうそう。たまに固まるんだよね〜これ。」
あるいは,わずかなダウンロード数しかないのに,「全ユーザのほとんどがダウンロードしました」とか小恥ずかしいプレスリリースをしてしまったり,課金に失敗してお詫びするという訳のわからんことをしてしまってそれ自体がニュースになるくらいのことです。

(追記)さらに,「C/C++」に限定します。(追記おわり@2005/04/15)

volatile:
あくまでもこれは,コンパイラへのお知らせであって,「同期」とは関係なさそうです。
ぶっちゃけて,
 ・対象変数をメモリに置け(レジスタに配置するな)
 ・対象変数を読む時にはメモリから読め
ということを最適化さんにお願いするだけです。

逆に言うと,volatileを指定しない変数へのアクセスが最適化によって省略される可能性があるです。

int a =0;

a++;

a++;

が,

int a=2;

というように最適化されるかもしれません。「だから何なの?」と思う人は使わんで宜しいです^^

マイコンで開発する人,特にOSレス環境あるいは,プロセス間通信またはスレッド間通信を共有したメモリ(いわゆる共有メモリではなく,適当な同一のメモリに複数のコンテキストからアクセスするという意味)で行うような人は,コンパイラが吐くコードを見てきちんと理解せねばならないでしょう。

確認方法は,コンパイラが吐いたコードを読むことです。アセンブラソースを吐かせても良いですし,バイナリを読んでも良いです。

ハマリポイントとしては,volatileを使わない状態で,最適化オプションを変えると挙動が変わる場合があることです。

当然のことながら,マルチプロセッサがどうのこうのとかは全く関係ないです。
また,本質的には同期とは関係ありませんが,下に書くメモリの同期という意味では関連がありますが,volatileだけでメモリの同期を実現するのは,ある意味簡便な書き方で,混乱の種をまくことになるのかもしれません。

さらにメモリキャッシュとも関係がありません。コンパイラがもしvolatileに対してキャッシュのフラッシュをおこなうようなコードを吐くなら話は別ですが,そんなコンパイラは存在しなさそうです。

ちなみにPOSIX環境では,volatileは使わなくても良いようです。
http://www.lambdacs.com/cpt/FAQ.html#Q56
参照。
・・・WindowNT系って,POSIXサブシステム入ってたよな〜(独り言)

同期:
同期という単語が広くいろんな意味で使われることがそもそも問題です。
同期を,「動作の同期」と絞って考えるべきでしょう。カビ臭い計算機の教科書では,「資源利用の排他制御」のように書かれています。

・・・・
話をややこしくする原因のひとつが,「ロック」という言葉です。個人的には,CGIでのperlプログラマが「fopenロック」とかわけのわからんことを言い出した辺りが諸悪の根源と睨んでいるのですが・・・「ロックとアンロック」というセットであれば,下に出てくる「資源の占有と開放」で解釈可能です。というか,lockという言葉は排他制御をイメージ的に伝えるために使われるものでは無いのでしょうか。同じ概念を指すのに,別の言葉を使うのは気が進みません。
・・・・

同時に利用してはいけない資源に対して,複数のプロセッサ,プロセス,スレッドなどからの利用(アクセス)を1個ずつに制限する,ということです。

例えば,ひとつのファイルに複数のプロセスが追記していくような場合。あるプロセスがファイルに追記している間,他のプロセスは,「ファイルという資源が解放されるのを待つ」わけです。開放されたら「資源を占有」して,ファイルに追記します。

イメージとしては,動作の同期を「あるまとまった動作の塊」と考えるのが良いでしょう。この例からもわかるように同期には「資源の占有開始」と「資源の解放」という2つで1組の操作が必要となります。ミューテクスやらセマフォやら種類がたくさんあるのは,応用別に使いやすくした等価変形であって,本質的には同じでしょう。

よくある誤解は,「動作の同期」を「変数(メモリ内容)の同期」と誤解していることです。これは,もう少し掘り下げて考えると良いでしょう。
おそらくメモリの同期とは,複数のプロセッサ,プロセス,スレッドなどからの
 ・メモリからのリードやライトなどのアクセスひとまとまり
単位で,矛盾が発生させないこと(の総称)でしょう。これに動作の同期を適用するには,
 ・ひとまとまりがどこからどこまでか
を見抜いて,同期操作(占有と開放)を適切に設置します。

同期操作は,OSの基本的な機能ですから,OSレス環境では自前で書かねばなりません。しかしながらプロセス,スレッドなどのコンテキストスイッチあるいはタスクディスパッチも自前でしょうから,それほど難しくはありません。

OSに同期操作が用意されていれば,実行環境についてだけ,よく調べて使えばさして問題はないでしょう。

当然のことながら,マルチプロセッサがどうのこうのというのは,OSが対応しているかどうかに拠ります。

メモリキャッシュ:
メモリキャッシュは,プロセッサのメモリへのアクセスをキャッシュするものです。すなわち,
 ・プロセッサはメモリにストアする命令を実行したが,
 ・ストアされたデータは未だキャッシュにみ存在し
 ・メモリ上には古いデータが存在し続けている状態
があり得ます。ロードにおいても同様です。

すなわち,同じメモリからロードしても,異なるプロセッサでは異なるデータをロードしてしまう可能性が常に存在するということです。

では異なるプロセスではどうかというと,これはOSのメモリ管理機構に従って挙動が異なるでしょう。また,プロセスのモデルによっても異なるかもしれません。つまり仮想メモリと呼ばれる仕組みに影響されるでしょう。

一般的にメモリ管理機構があるならば,プロセスごとに別々のメモリ空間で動作するでしょう。ではプロセス間で共有されたメモリにおけるメモリキャッシュはどうなるでしょうか。おそらく,同一の物理メモリアドレスへのアクセスでは,異なるプロセスからのアクセスがキャッシュに邪魔されることは無いでしょう。同一でない物理メモリアドレスに共有メモリが配置される場合があるかどうかについては実際の所よくわかりません。使用する共有メモリについてよく調べて使えばさして問題ないでしょう。大抵は同期操作についての指示があるはずです。すなわち,共有メモリを資源とみなして,上で書いた同期について考慮すれば問題ないでしょう。

異なるプロセッサ間でキャッシュに邪魔されないためには,プロセッサに備えられている機構を用います。あるいはそれをラップして利用しやすくしたOSの機能を用います。例えば,WindowsにおけるInterlocked〜〜〜のようなものがあるようです。

仮想メモリ:
メモリ管理機構が仮想メモリを実現している場合,非常にややこしい話になります。
挙動はOSの実装だけでなく,ハードウェアの構成にも左右されるので(というかごみためまんはそこまで考えたことが無い)考慮すべき事柄についてだけメモします。
・ページングのタイミング(おそらく隠蔽かされて見えないはず)
・マルチプロセッサ
 ・物理メモリをプロセッサ間で共有している場合
 ・物理メモリをプロセッサ間で共有していない場合(メモリバスも独立)

ただし,上記は「プロセッサから見て共有されているかどうか」が問題です。チップセットなどにより,透過的に見えたりするのであれば,さらに考慮すべき事柄がひとつ増えることになります。メモリキャッシュがもう1段増える感じです。その考慮をハードウェアのみで隠蔽するのか,ソフトウェアも考慮しなければいけないのかなど,オプションは多彩です。

この話と,「異なるプロセスからみれば共有メモリも異なるアドレスに配置される」というのは別の話です。共有メモリが異なるアドレスに,という文脈でのアドレスはプロセス毎の「仮想アドレス」です。物理アドレスが同一であれば,プロセッサからは同じメモリに見えるはずです。ここでメモリキャッシュと絡めて考えるともう何がなんだか分からない話になります。

・・・・
ごみためまん的には,ここまで考えておいて,やっとこさハイパースレッドがどうとかSMPがどうとかいう話についていける気がします。

あと上記でもvolatileのところで触れてありますが,OSの実装あるいはOSの有無が大きく関係します。だからいわゆるPC環境とマイコン環境では話がかみ合わないはずです。さらに,厄介なことに,最近はマイコン環境でも偉そうなOSが搭載されているので混乱に拍車がかかっているのでしょう。さらにさらに不適切に移植されたりテストされていないgccの移植版がこれだけ氾濫してくると,コンパイラの挙動にもハメられている場面が多かろうとも思われます・・・(だんだん気がめいってきた;;)

ついでに書いておきますが,Linuxの内部実装に触れたような記述において,スピンロックがどうのこうのとかいう議論をユーザアプリケーションレベルでの議論に無理やり持ってくることが散見されます。これも混乱のひとつの原因かと思われます。同様に,Windows界隈では,ドライバを書く用の排他の話が混ざってきます。視点が高い(あるいは深い)人ほど,混乱を呼び込んでいるような気がします。

個人的には,排他や同期は言語レベルでサポートしてくれないと困ります。なぜならばソフトウェア技術者にハードウェア,それも実世界のプロセッサの周辺についての知識を求めるのはもう無理です。変数をメモリ上のデータと理解していない人がこれだけ増えてしまった現在,「変数への代入をメモリアクセスとして考えろ」というのは無謀というかむしろ,石頭のおじいちゃんのグチにしか聞こえません。

排他に失敗してユーザアプリがデッドロックを起こしたり,カウントに失敗したり,ファイルを破壊してしまったりしても,今時のOSは平気で動作し続けるからです。たとえ終了しなくなってしまっても,killすりゃいいでしょ,タスクマネージャで強制終了させりゃいいでしょ?排他の失敗による障害は,そもそも発生頻度が低いはず。原因究明よりも再起動優先でしょう?

そんな甘っちょろいことが許されないと思う人は適当にがんばってください。どうせその仕事を後で誰かが引き継いだときにはその精神はすっかり忘れ去られてしまっていることでしょうが。

リンクル:
Synchronization and Multiprocessor Issues
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/synchronization_and_multiprocessor_issues.asp
これはマルチプロセッサでの問題点を指摘しており,おそらくマルチプロセスでは問題にならないと思われます。

Warning! Threading in a multiprocessor world
http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-toolbox_p.html
これもマルチプロセッサでの話。

ごみためのランキングはこちら。