仮想共有メモリ
共有メモリと分散メモリ
特別な目的のマルチプロセッサハードウェアには2つのタイプがある.「共有メモリ」と「分散メモリ」である.共有メモリマシンでは,すべてのプロセッサは共通のメインメモリにアクセスできる.分散メモリマシンでは,各プロセッサは独自のメインメモリを持っており,プロセッサは高度なネットワークで接続される.ネットワークで繋がれたPCの集まりは分散メモリ並列マシンの一種である.
プロセッサ間の通信は,小さい並列処理タスクを除いては重要な必須条件である.共有メモリマシンでは,一つのプロセッサが特定のメモリの場所に値を書き込むだけで,他のすべてのプロセッサがその値を読むことができる.分散メモリマシンでは,変数の値の交換にはネットワークを介した明示的な通信が必要となる.
仮想共有メモリ
「仮想共有メモリ」は分散メモリマシン上のプロセッサが共有メモリを持っているかのようにプログラムできるプログラミングモデルである.ソフトウェア層が必要な通信を透過的に行う.
Wolfram言語は並列プロセッサとして独立のカーネルを使う.これらのカーネルは,たとえ同じマシン上にあっても,共通のメモリを共有しないことは明らかである.しかし,Wolfram言語はこれらのリモートカーネルに仮想共有メモリを実装する関数を提供するのである.
これは簡単なプログラミングモデルで行う.変数 a が共有されているとすると,(評価することによって)この変数を読むカーネルはすべてマスターカーネルが管理する共通の値を読む.a=val 等を割り当てることによって a の値を変更するカーネルはすべて,変数 a の大域コピーを変更し,他のすべてのカーネルが続いて変数を読むときに,新しい値が見られるようにする.
共有変数の短所は,読取り,書込みの全操作にネットワークを介した通信が必要になることである.そのため,ローカルの共有ではない変数へのアクセスよりは時間がかかる.
共有変数と関数の宣言
SetSharedVariable[s1,s2,…] | シンボル si を共有変数として宣言する |
SetSharedFunction[f1,f2,…] | シンボル fi を共有関数またはデータ型として宣言する |
コマンドSetSharedVariableは属性HoldAllを持っており,これで通常値を持つ与えられた変数の評価を避ける.
SetSharedVariableまたはSetSharedFunctionを使うと,現在接続されている,あるいは新規に起動されたリモートカーネルはすべて,マスターカーネルを介して共有変数にアクセスする.
$SharedVariables | 現在共有されている変数のリスト(Hold[]で囲まれている) |
$SharedFunctions | 現在共有されている関数のリスト(Hold[]で囲まれている) |
UnsetShared[s1,s2,…] | 指定の変数または関数の共有を停止する |
UnsetShared[patt] | 文字列パターン patt に合致する名前を持つ変数と関数の共有を停止する |
カーネルをParallel`Developer`ClearSlaves[]でクリアすると,共有変数と下向きの値もすべてクリアされる.
共有変数
SetSharedVariable[s]で共有を宣言された変数 s はマスター(ローカル)カーネルにのみ存在する.リモートカーネルでの以下の操作は,記述された効果が得られるように再定義される.
s | 変数を評価するときは変数の現在の値をマスターカーネルに尋ねる |
s=e,s:=e | 値を s に割り当てると,マスターカーネルで割当てが行われる |
s++,s--,++s,--s,s+=k,s-=k,s*=k,s/=k,AppendTo[s,k] | インクリメント,デクリメント操作はマスターカーネルで行われる(この操作は原子的であり,同期に使用することができる) |
Part[Unevaluated[s],i] | s の部分を抽出する.この操作では,s のすべての値ではなく,要求された部分のみがWolfram Symbolic Transfer Protocol(WSTP)接続を介して送信される |
s[[i]]=e | 変数の指定部分を新しい値で置換する.古い値 s は部分割当てが許可されるために必要な構造を持たなければならない |
技術的な理由により,すべての共有変数は値を持たなければならない.マスターカーネルの変数が値を持たない場合はNullに設定される.
副条件を使う条件付き割当て等の他の形式の割当てはサポートされていない.
部分抽出の慣習的な形式 s[[i]]はスレーブカーネルに s の全値を送信する.i 番目の部分のみを送信するにはPart[Unevaluated[s],i]を使う.
共有を宣言するときに変数がProtectedなら,リモートカーネルは変数にアクセスすることはできるが,値を変更することはできない.
基本的な例
最低2つのリモートカーネルが実行中でなければならない.使用を簡単にするために,それらを2つの変数に割り当てる.
カーネルr1はこれでxの共通の値にアクセスできるようになった.
共有関数
SetSharedFunction[f]で共有を宣言されたシンボル f はマスター(ローカル)カーネルにのみ存在する.リモートカーネルでの以下の操作は,記述された効果が得られるように再定義される.
f[i],f[i,j],… | 関数や配列の要素 f[i] 等を評価するときは,シンボルの現在の下向きの値をマスターカーネルに尋ねる |
f[i]=e,f[i,j]=e,f[i]:=e,… | f[i] 等に値を定義すると,マスターカーネルでも同じ定義が行われる. |
f[[i]]++,f[[i,j]]--,++f[[i]],--f[[i]] | インクリメント,デクリメント操作はマスターカーネルで行われる(この操作は原子的であり,同期に使用することができる) |
技術的な理由により,f[…]という形式の式はすべて値を持たなければならない.マスターカーネルの式f[…]が評価しない場合,結果はNullに設定される.
副条件を使う条件付き割当て等の他の形式の割当てはサポートされていない.
共有関数は以下のように定義できる.シンボルxがリモートカーネルでもマスターカーネルでも値を持っていないようにしなければならない.シンボルxは共有変数であってはならない.
リモートカーネルで遅延割当てを行うなら,定義の右辺は関数を使うカーネルで評価される.即時割当てはすべてマスターカーネルで評価される.
指標付き変数または配列は, x[1],x[2]等の形式の共有の下向きの値を使って実装できる.
共有を宣言するときに関数がProtectedなら,リモートカーネルはそれを使うことはできるが,定義を変更することはできない.
同期
いくつかの並行実行中のリモートカーネルが,読み書きのために同じ共有変数にアクセスするような場合は,値を読んだときと新しい値を書き込むときの間に変数の値が別のプロセスによって変更されていないことは保証できない.その間に別のプロセスが書き込んだ新しい値はすべて上書きされる.
例:危険域
共有変数へのアクセスが制御されていない場合についてのこの古典的な例で問題を示す.この例を試すためには,2個から10個のリモートカーネルを実行する必要がある.
ParallelMapの第1引数の中のコードは,利用できるリモートカーネルで別々に実行されるクライアントコードである.このコードは共有変数 y を読み,その値をローカル変数 a に保管し,計算を行い(ここではPause),y の値を a+1に設定してインクリメントしたいとする.しかしそのときまでに,y の値は他のプロセスによって変更され,おそらく a ではなくなっている.
このコードが(ParallelMapをMapに変更することで)順次実行されると,y の最終値は10になるが,十分な並列プロセスを使うと,おそらくこの値は小さくなる.
変数 y の値を読んでから,それに新しい値を設定するまでの間のコードは「危険域」と呼ばれる.その部分の実行中は別のプロセスは y の値を読み書きするべきではない.プロセスは危険域を独占するために,危険域に入る前に独自のロックを行い,危険域を去るときにロックを解除することができる.
Wolfram言語はロックを行い,exprを評価し,ロックを解除するための関数CriticalSection[lck,expr]を提供している.あるプロセスがロックを行ったら,他のプロセスは行えない.ロックは式の評価が終了したら解除される.
以下は前の例と同じであるが,ロックを実装するコードが追加されている.カーネルはロックに失敗したら,成功するまで繰返し試みる.
プロセスは(While内で)ロックを得ようと試みるたびに,その後しばらく待機する.待機しないと,別のプロセスが独占しているロックを得ようと待っているプロセスがマスターカーネルに負荷をかけることになる.
ロックを利用すると,リモートプロセスが他のプロセスを待つ可能性があるため,計算速度が落ちる.この例では,結果は基本的には順次実行である.危険域はできるだけ短くしなければならない.2つのプロセスがそれぞれロックを持っており,互いのロックを得ようとしていると,「デッドロック」が起り,プロセスは無限に待機することになる.
この例の計算のトレース
共有変数の操作のデバッグには,ツールキットをロードする前にデバッグパッケージをロードしてトレースを有効にすることができる.