[ 目次, 前節, 次節, 索引 ]

3.4.2 並列性の制御機構



並列的プロセスを扱う困難の根源が, 異るプロセスでの事象の順の混ざり合いを考慮する必要にあることを見た. 例えば一つが(a, b, c)の順序の三つの事象を持ち, もう一つが(x, y, z)の順序の三つの事象を持つ二つのプロセスがあるとする. 二つのプロセスが並列的に走り, 実行が混ざり合うのに, なんの制約もなければ, 二つのプロセスのそれぞれの順は正しくなっているような事象の順は20通りある:
(a,b,c,x,y,z) (a,x,b,y,c,z) (x,a,b,c,y,z) (x,a,y,z,b,c)
(a,b,x,c,y,z) (a,x,b,y,z,c) (x,a,b,y,c,z) (x,y,a,b,c,z)
(a,b,x,y,c,z) (a,x,y,b,c,z) (x,a,b,y,z,c) (x,y,a,b,z,c)
(a,b,x,y,z,c) (a,x,y,b,z,c) (x,a,y,b,c,z) (x,y,a,z,b,c)
(a,x,b,c,y,z) (a,x,y,z,b,c) (x,a,y,b,z,c) (x,y,z,a,b,c)
このシステムを扱うプログラマは, 20種の順のそれぞれの効果を考え, それぞれの振舞いが受け入れられるかを調べなければならない. プロセスと事象の数が増えるとこういうやり方は急に面倒になる.

   もっと実用的な並列システムの設計法は, プログラムの振舞いが正しいことが保証出来るように, 並列プロセスの混ざり合いを制約する一般的な機構を考え出すことである. この目的のために多くの機構が開発された. 本節では, その一つ, 直列変換器(serializer)を説明する.

共有状態へのアクセスの直列化
直列化は次の考えを実装する: プロセスは並列的に実行するが, 並列には実行出来ない手続きの集りがある. より正確には, 直列化は特別な手続きの集合を作り出し, その直列化した集合のそれぞれからは一時に一つの手続きだけ実行することが許される. 集合の中の一つの手続きが実行中だと, その集合の任意の手続きを実行しようとするプロセスは, 前の実行が終るまで待たされる.

   共有変数へのアクセスの制御に直列化を使うことが出来る. 例えば共有変数を, その変数の前の値に基づいて更新しようと思えば, その変数の前の値へのアクセスと, その変数への新しい値の代入とを, 同一の手続きの中に置く. そうすれば, その変数に代入する他の手続きは, これらの手続きを同一の直列変換器で直列化することで, この手続きと並列的には走れないことが保証出来る. そしてこの変数の値はアクセスと対応する代入の間に変更されないことが保証出来る.

Schemeの直列変換器
上の機構を更に具体的にするため, Schemeを拡張して parallel-executeという手続きを取り入れたとしよう.

(parallel-execute  ⟨p1⟩  ⟨p2⟩   ...    ⟨pk)

各⟨p⟩は引数なしの手続きである. parallel-executeは各⟨p⟩に, 別々のプロセスを作り出し, それが(引数なしに)⟨p⟩を作用させる. これらのプロセスはすべて並列的に走る.40

   この使い方の例として

(define x 10)

(parallel-execute (lambda () (set! x (* x x)))
                  (lambda () (set! x (+ x 1))))
を考えよう. これは並列プロセスを二つ---xx掛けるxに設定するP1と, xを増加させるP2---を作り出す. 実行が完了した時, xP1P2の事象の混ざり合いに依存して五つの可能な値の一つになっている:

101:     P1xを100に設定し, 次にP2xを101に増加する.
121: P2xを11に増加し, 次にP1xx掛けるxに設定する.
110: P1(* x x)の評価でxの値に二度アクセスする間にP2xを10から11に変える.
11: P2xにアクセスし, P1xを100に設定, P2xを設定する.
100: P1xに(二回)アクセスし, P2xを11に設定し, P1xを設定する.

   直列変換器(serializer)で作り出された直列化手続きを使い, 並列性を制約することが出来る. 直列変換器はmake- serializerにより構成する. その実装は下に示す. 直列変換器は引数として手続きをとり, 元々の手続きと同様に振舞う直列化した手続きを返す. 与えられた直列変換器への呼出しはすべて同一の集合の直列化手続きを返す.

   そこで上の例とは対照的に

(define x 10)

(define s (make-serializer))

(parallel-execute (s (lambda () (set! x (* x x))))
                  (s (lambda () (set! x (+ x 1)))))
ではxの可能な値として101と121の二つしか生じない. P1P2は混ざり合うことは出来ないので, 他の可能性は排除される.

   次は3.1.1節のmake-account手続きで, 預入れと払出しが直列化された版である:



(define (make-account balance)
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (let ((protected (make-serializer)))
    (define (dispatch m)
      (cond ((eq? m 'withdraw) (protected withdraw))
            ((eq? m 'deposit) (protected deposit))
            ((eq? m 'balance) balance)
            (else (error "Unknown request -- MAKE-ACCOUNT"
                         m))))
    dispatch))
この実装では, 二つのプロセスは, 一つの口座で並列的に払い出したり預け入れたりは出来ない. そして図3.29に示した, Paulが新しい値を計算するため残高にアクセスした時と, Paulが実際に残高に代入した時との間にPeterが残高を更新するというエラーの源は排除された. 他方, 各口座は自分の直列変換器を持ち, 異る口座の預入れと払出しは並列的に進めることが出来る.

問題 3.39


上に示した並列実行の五つの可能性のうち, 次のように実行を直列化すると, どれが残るか:
(define x 10)

(define s (make-serializer))

(parallel-execute (lambda () (set! x ((s (lambda () (* x x))))))
                  (s (lambda () (set! x (+ x 1)))))


問題 3.40


次の
(define x 10)

(parallel-execute (lambda () (set! x (* x x)))
                  (lambda () (set! x (* x x x))))
の実行の結果となり得るxの可能性をすべて述べよ. 直列化した手続き:
(define x 10)

(define s (make-serializer))

(parallel-execute (s (lambda () (set! x (* x x))))
                  (s (lambda () (set! x (* x x x)))))
を使うと, これらの可能性のうち, どれが残るか.

問題 3.41


Ben Bitdiddleは, 銀行口座への直列化しないアクセスを許すと, 不正な振舞いになるかも知れないので, 銀行口座を次のように実装する方がよいのではないかと考えた(コメントの行が変更されている):

(define (make-account balance)
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (let ((protected (make-serializer)))
    (define (dispatch m)
      (cond ((eq? m 'withdraw) (protected withdraw))
            ((eq? m 'deposit) (protected deposit))
            ((eq? m 'balance)
             ((protected (lambda () balance)))) ; 直列化した.
            (else (error "Unknown request -- MAKE-ACCOUNT"
                         m))))
    dispatch))
賛成するか. Benの心配を示すシナリオはあるだろうか.

問題 3.42


Ben Bitdiddleはwithdrawdepositのメッセージのそれぞれに応じて新しく直列化された手続きを作り出すのは時間の浪費だと考えた. 彼がいうにはmake-accountを変更し, protectedの呼出しはdispatch手続きの外でするように出来る. つまり口座は払出し手続きから呼び出される度に, (口座と同時に作り出された)同一の直列化された手続きを返す.

(define (make-account balance)
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (let ((protected (make-serializer)))
    (let ((protected-withdraw (protected withdraw))
          (protected-deposit (protected deposit)))
      (define (dispatch m)
        (cond ((eq? m 'withdraw) protected-withdraw)
              ((eq? m 'deposit) protected-deposit)
              ((eq? m 'balance) balance)
              (else (error "Unknown request -- MAKE-ACCOUNT"
                           m))))
      dispatch)))
これは安全な変更か. 特に二つのmake-accountの版で許される並列性の間には何か違いがあるだろうか.
複数の共有資源を使う複雑さ
直列変換器は並列プログラムの複雑さを隔離し, プログラムが注意深く, また(望むらくは)正しく扱われるようにするのを助ける強力な抽象を提供する. しかし(単一の銀行口座のような,) 単一な共有資源だけが存在する時は, 直列変換器を使うことは比較的直截であるが, 並列プログラムは共有資源が多くなると期待に反して困難になり得る.

   起きそうな困難の一つを例示すれば, 二つの銀行口座の残高を交換したかったとしよう. それぞれの口座にアクセスし, 残高の差を計算し, 一方の口座から差を払い出し, それをもう一方の口座に預け入れる. これは次のように実装出来よう:41



(define (exchange account1 account2)
  (let ((difference (- (account1 'balance)
                       (account2 'balance))))
    ((account1 'withdraw) difference)
    ((account2 'deposit) difference)))

   この手続きは一つのプロセスだけが交換しようと試みる時はうまく働く. しかしPeterとPaulが口座a1, a2とa3にアクセス出来, Peterがa1とa2を交換し, Paulがa1とa3を並列的に交換するとしよう. 個々の口座について預入れと払出しが(本節で示したmake-accountのようにして)直列化された口座でも, exchangeは正しくない結果を生じ得る. 例えばPeterがa1とa2の残高の差を計算しようとするが, Peterが交換を完了する前にPaulがa1の残高を変更するかも知れない.42 正しい振舞いにはexchange手続きのために, 変換の全時間にわたって, その口座に並列的にアクセスするのを排除する配慮が必要である.

   これを達成する一つの方法は, 両方の口座の直列変換器を使い, 全体のexchange手続きを直列化することである. それには口座の直列変換器にアクセスする方法を作る. 直列変換器を露出させ, 銀行口座オブジェクトの部品化力をわざと壊したことに注意しよう. make-accountの次の版は3.1.1節にあった元々の版とは, 直列変換器が残高変数を守るために用意してあることと, メッセージパッシングを経由して直列変化器が輸出されていること以外は同じである:


(define (make-account-and-serializer balance)
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (let ((balance-serializer (make-serializer)))
    (define (dispatch m)
      (cond ((eq? m 'withdraw) withdraw)
            ((eq? m 'deposit) deposit)
            ((eq? m 'balance) balance)
            ((eq? m 'serializer) balance-serializer)
            (else (error "Unknown request -- MAKE-ACCOUNT"
                         m))))
    dispatch))

   これを使って預入れと払出しが直列化出来る. しかし以前の直列化した口座と違い, 今度は次の例のように, 直列化を明示的に管理するのは銀行口座オブジェクトの各利用者の責任である:43


(define (deposit account amount)
  (let ((s (account 'serializer))
        (d (account 'deposit)))
    ((s d) amount)))

   直列変換器をこのように輸出すると, 直列化した変換プログラムの実装に十分な自由度が得られる. 元々のexchange手続きを両口座の直列変換器で直列化する.


(define (serialized-exchange account1 account2)
  (let ((serializer1 (account1 'serializer))
        (serializer2 (account2 'serializer)))
    ((serializer1 (serializer2 exchange))
     account1
     account2)))

問題 3.43


三つの口座の初めの残高が10ドル, 20ドルおよび30ドルで, 口座の残高を交換しながら複数のプロセスが走るとしよう. プロセスが逐次的に走ったなら, 任意の回数の並列の交換の後で, 口座の残高はある順序で10ドル, 20ドルおよび30ドルであるべきことを論ぜよ. 図3.29のようなタイミング図を描き, 本節の口座交換プログラムの最初の版を使って交換を実装すると, この条件が破られ得ることを示せ. 他方, このexchangeプログラムを使っても, 残高の合計は保存されることを論ぜよ. タイミング図を描き, 各口座の取引きを直列化しなければ, この条件さえも破れることを示せ.

問題 3.44


お金を口座から口座へ移す問題を考えよう. Ben Bitdiddleは例えば本文にあったmake-accountの版のような, 預入れと払出しの取引きを直列化する口座の機構を使えば, 多くの人が多くの口座の間で並列的にお金を移したとしても, 次の手続きで達成出来ると主張した.
(define (transfer from-account to-account amount)
  ((from-account 'withdraw) amount)
  ((to-account 'deposit) amount))
Louis Reasonerは, ここに問題があり, 交換の問題を扱うのに必要だったような,より巧妙な方法を使う必要があると主張する. Louisは正しいか. そうでなければ, 移動の問題と交換の問題の間にはどういう本質的な違いがあるか. (from-accountの残高は, 少なくてもamountであると仮定せよ.)

問題 3.45


Louis Reasonerの考えでは, われわれの銀行口座システムは, 預入れと払出しが自動的には直列化出来ないので, 必要以上に複雑だしエラーが起き易い. 彼は(make-accountがやったように)口座と預入れを直列化する(代りにではなく,)のに加え, (serialized-exchangeのような手続きが使うために) make-account-and-serializerが直列変換器を輸出すべきであるという. 彼は口座を次のように再定義するよう提案する:
(define (make-account-and-serializer balance)
  (define (withdraw amount)
    (if (>= balance amount)
        (begin (set! balance (- balance amount))
               balance)
        "Insufficient funds"))
  (define (deposit amount)
    (set! balance (+ balance amount))
    balance)
  (let ((balance-serializer (make-serializer)))
    (define (dispatch m)
      (cond ((eq? m 'withdraw) (balance-serializer withdraw))
            ((eq? m 'deposit) (balance-serializer deposit))
            ((eq? m 'balance) balance)
            ((eq? m 'serializer) balance-serializer)
            (else (error "Unknown request -- MAKE-ACCOUNT"
                         m))))
    dispatch))
そして預入れは元々のmake-accountでやったように扱う:
(define (deposit account amount)
 ((account 'deposit) amount))
Louisの考えで何が悪いか説明せよ. 特にserialized-exchangeを呼び出した時, 何が起きるか考えよ.
直列変換器の実装
直列変換器は 相互排除器(mutex)という, より基本的な同期機構を使って実装出来る. 相互排除器は二つの演算---相互排除器を 獲得(acquire)出来る. 相互排除器を 解放(release)出来る---を提供するオブジェクトである. ある相互排除器が獲得されると, それが解放されるまで, その相互排除器は誰にも獲得出来ない.44 われわれの実装では, 各直列変換器に相互排除器が対応する. 手続きpが与えられると, 直列変換器は, 相互排除器を獲得し, pを走らせ, 相互排除器を解放する手続きを返す. これにより直列変換器の作った手続きの内, 一時には一つだけしか走らないことが確実になり, これは保証を必要とする直列化の性質に他ならない.

(define (make-serializer)
  (let ((mutex (make-mutex)))
    (lambda (p)
      (define (serialized-p . args)
        (mutex 'acquire)
        (let ((val (apply p args)))
          (mutex 'release)
          val))
      serialized-p)))

   相互排除器は真か偽の値を保持する可変オブジェクトである. (ここでは一要素のリストを使う. これを セル(cell)と呼ぶことにする.) 値が偽の時, 相互排除器を獲得することが出来る. 値が真の時, 相互排除器は獲得出来ず, 相互排除器を獲得しようとするプロセスは待たされる.

   相互排除器構成子make-mutexはまずセルの内容を偽に初期化する. 相互排除器を獲得するにはセルをテストする. 相互排除器が獲得出来るなら, セルの内容を真に設定して先へ進む. そうでなければ獲得しようと何度も試みながら, 相互排除器が獲得出来るようになるまで, ループして待つ.45 相互排除器を解放するには, セルの内容を偽に設定する.


(define (make-mutex)
  (let ((cell (list false)))            
    (define (the-mutex m)
      (cond ((eq? m 'acquire)
             (if (test-and-set! cell)
                 (the-mutex 'acquire))) ; retry
            ((eq? m 'release) (clear! cell))))
    the-mutex))

(define (clear! cell)
  (set-car! cell false))

   test-and-set!はセルをテストし, テストの結果を返す. その上, テストが偽なら, test-and-set!は偽を返す前にセルの内容を真に設定する. この振舞いを次の手続きで表すことが出来る:


(define (test-and-set! cell)
  (if (car cell)
      true
      (begin (set-car! cell true)
             false)))

   しかしtest-and-set!のこの実装は見たほどには十分ではない. ここには重要な微妙さがあり, それは並列性制御がシステムに登場する本質的な場所である: test-and-set! 不可分に(atomically)実行しなければならない. つまりあるプロセスがセルをテストし, 偽であることを見つけたら, 他のプロセスがセルをテストする前にセルの内容は実際に真に設定されるということを保証しなければならない. この保証がないと図3.29で銀行口座が破綻したのと同じように相互排除器は破綻する. (問題3.46参照)

   test-and-set!の実際の実装はシステムが並列プロセスを走らせる方法の細部に依存する. 例えば並列プロセスを逐次プロセスで, 各プロセスを短時間走らせては中断し,次のプロセスへ進むという 時間切片プロセス循環機構を使って実装しているかも知れない. こういう場合はtest-and-set!はテストと設定の間で時間切片が変らないようにすることでうまくいく.46 またマルチプロセスの計算機は, 不可分の演算をハードウェアで直接支援する命令を用意している: 47

問題 3.46


test-and-set!を演算を不可分にしようとは試みず, 本文に示したような通常の手続きを使って実装したとしよう. 図3.29のようなタイミング図を描き, 二つのプロセスが同時に相互排除器を獲得するのを許すと, 相互排除の実装が破綻することを示せ.

問題 3.47


(大きさnの)セマフォは相互排除器の一般化である. 相互排除器と同様に, セマフォは獲得と解放の操作を提供するが, 最大n個のプロセスまで, それを並列的に獲得出来るという点で, より一般的である. そのセマフォを獲得しようとするそれ以上のプロセスは解放操作を待たなければならない. セマフォを

a. 相互排除器を使って

b. test-and-set!を使って

実装せよ.

デッドロック
直列変換器の実装は見たが, 口座の交換には上のserialized-exchange をもってしても問題がある. Paulがa2をa1と交換しようとしている時に, Peterがa1をa2と交換しようとしたとする. Peterのプロセスが直列変換器に入り, a1を保護するところまで到達し, その直後にPaulのプロセスがa2 を保護する直列化手続きに入ったとしよう. PeterはPaulがa2を保護する直列化手続きを出るまで(a2を保護する直列化手続きへ入ろうとして)進むことは出来ない. 同様にPaulもPeterがa1を保護する直列化手続きを出るまで進むことが出来ない. 各プロセスは相手を待ちながら, 永久に立往生する. この状態をデッドロック(deadlock)という. デッドロックは多くの共有資源に並列アクセスを許すシステムでは常に危険である.

   この状態でデッドロックを回避する一つの方法は, 各口座に独自の識別番号をつけ, 少ない番号の口座を保護する手続きの方が, 先に入ろうとするよう, serialized-exchangeを書き直すことである. この方法は, 交換問題ではうまく働くが,より巧妙なデッドロック回避技法を必要とする状況や, デッドロックが全く回避出来ない状況も存在する. (問題3.48と3.49参照) 48

問題 3.48


上に述べたデッドロック回避法(つまり口座に番号をつけ, プロセスはより小さい番号の方の口座を先に獲得しようとする.)が交換問題でデッドロックを回避する理由を詳しく説明せよ. この方法を採用するようserialized-exchangeを書き直せ. (make-accountも書き直し, 各口座は番号と共に作り出され, その番号は適切なメッセージを送ってアクセス出来るようにしなければならない.)

問題 3.49


上に述べたデッドロック回避機構が働かないようなシナリオを考えよ. (ヒント: 交換問題では, 各プロセスはどの口座にアクセスしなければならないか, 前もって知っている. プロセスが, 次にどの共有資源が必要になるか知る前にある共有資源にアクセスしなければならない状況を考えよ.)

並列性, 時および通信
並列システムのプログラミングでは, 異るプロセスが共有状態にアクセスする事象の順を制御する必要があることを見, またこの制御を直列変換器の上手な使い方で達成出来るのも見た. しかし並列性の問題は, 基本的な観点から「共有状態」が何を意味するか常には明らかでないので, これより更に深い所にある.

   test-and-set!のような機構は, プロセスが大域的共有フラグを任意の時刻に調べることを必要とする. これは問題であり, 最新の高速プロセッサに実装するのに効率的でない. そこではパイプラインやキャッシュメモリーのような最適化手法のため, メモリーの内容がすべての時点で首尾一貫した状態にあるとは限らない. そのため最近のマルチプロセッシングシステムでは, 直列変換器パラダイムは, 並列制御の新しい解決法に代られつつある.49

   共有状態の問題点は巨大分散システムにもある. 例えば各支店が地域の銀行口座の値を維持しており, 周期的にこれを他の支店が維持している値と比べているところを想像しよう. こういうシステムでは「口座の残高」という値は, 同期直後以外には決められない. Peterがお金をPaulと共同で持っている口座に預け入れたら, いつの時点で口座の残高が変ったというべきか. ---地域の支店の残高が変った時か, 同期が済むまでだめなのか. Paulが別の支店から口座にアクセスしたら, 振舞いが「正しく」なるためにおくべき正当な制約は何か. 正しいことに関係するのはただPeterとPaulが個別に見た振舞いと, 同期の直後の口座の「状態」である. 「真」の口座残高や同期間の事象の順序に関する質問は不適切か無意味である.50

   基本的現象は, 異るプロセスを同期させ, 共有状態を確定させ, 事象の順を強制するには, プロセス間の通信が必要だということである. 本質的には, 並列制御における時の概念は何でも通信に深く結びついている.51 時と通信の間の似たような関係が, (事象の同期をとるのに使える最速信号である)光の速度が時と空間に関連する基本的定数である 相対論にもあるのは面白い. 計算モデルで時と状態の扱いで出会った複雑性は, 実は物理的宇宙の基本的複雑性を反映している.


40 parallel-executeは標準Schemeの一部ではないが, MIT Schemeに実装することが出来る. われわれの実装では, 新しい並列プロセスも, 元々のSchemeのプロセスと並列的に走る. またわれわれの実装ではparallel-executeが返す値は新しく作り出されたプロセスを停止させるのに使える特別な制御オブジェクトである.

41 depositメッセージが負の値も受けつけるという事実を利用すれば, exchangeは単純化出来た. (これは銀行システムの重大な虫である.)

42 口座の残高が初めに10ドル, 20ドルおよび30ドルなら, 並列的な交換を繰り返した結果, 残高は相変らず, ある順序で10ドル, 20ドルおよび30ドルであるべきだ. 個々の口座の預入れを直列化しただけでは, これを保証するのに不十分である. 問題3.43参照

43 問題3.45は預入れと払出しが最早口座によって自動的には直列化されない理由を検討する.

44 「mutex」という用語は, 相互排除(mutual exclusion)の短縮形である. 並列プロセスに資源を安全に共有させる機構を整える一般的問題を相互排除問題という. 相互排除器は セマフォ (semaphore 腕木信号機)機構(問題3.47参照)の単純な変形である. セマフォは アイントホーフェン工科大学で開発され, 同大学のオランダ語のイニシアルで名づけられた "THE" マルチプログラミングシステムで紹介された (Dijkstra 1968a). 獲得と解放の演算は元々は Pと Vという. それは鉄道システムに使う腕木信号機との関係で, オランダ語passeren (通過する)とvrijgeven(解放する)からとられた. Dijkstraの古典的解説(1968b)は並列制御の論点を明確に示した最初のもので, いろいろな並列システムを扱うセマフォの使い方を示した.

45 殆んどの時分割の操作システムでは, 相互排除器で停止させられた プロセスは, 上のような 「ビジーウエイト」で時間を浪費はしない. そうではなく, 初めのプロセスが待つ間, システムは他のプロセスをスケジュールして走らせる. 停止させられたプロセスは, 相互排除器が獲得出来るようになると起こされる.

46 単一プロセッサ用の MIT Schemeは時間切片モデルを使っていて, test-and-set!は次のように実装出来る:

(define (test-and-set! cell)
  (without-interrupts
   (lambda ()
     (if (car cell)
         true
         (begin (set-car! cell true)
                false)))))
without-interruptsは, その手続きの引数の実行中, 時間切片が変らないようにする.

47 この種の命令には, テストセット, テストクリア, スワップ, 比較交換, ロードリザーブ, 条件つき格納を含め, いろいろある. これらの設計は, 機械のプロセッサと記憶装置のインターフェースにうまく合うようにしなければならない. ここで出てくる一つの論点は, 二つのプロセスがこういう機構を使い, 全く同じ時刻に同一の資源を獲得しようとすると, 何が起きるかを決定することである. どちらのプロセスが制御をとるか決める機構が必要になる. こういう機構を アービタ(arbiter調停者)という. 通常アービタはあるハードウェア装置へ任せることになる. アービタの決断まで, 適当に長い時間を許さなければ, 100パーセントの時間, 公平なアービタを物理的に構成することは出来ないと, 残念ながら証明することが出来る. その基本的な現象は十四世紀, フランスの哲学者 Jean Buridanが Aristotleの De caeloの注釈の中で認めた. Buridanは二つの等しい食糧の魅力的な源の間に置かれた完全に理性的な 犬は, どちらに初めに行くか決めることが出来ず, 餓死すると論じた.

48 共有資源に番号をつけ, それを順に獲得することでデッドロックを避ける一般技法は Havender(1968)による. デッドロックが回避出来ない状況は, デッドロック回復(deadlock recovery)法を必要とする. これはデッドロックの状態からプロセスを「後退」させ, もう一度試みさせる. デッドロック回復機構はデータベース管理システムで広く用いられる. Gray & Reuter 1993で詳しく論じられている話題である.

49 直列化に代るものの一つは バリア同期(barrier synchronization)という. プログラマは並列プロセスを好きなように走らせるが, ある同期点(「バリア」)を設けておく. すべてのプロセスがバリアに到達するまで, どのプロセスもそこを越えて進むことは出来ない. 最近のプロセッサには, プログラマが一貫性を要求された場所に, 同期点を設けるための機械命令が用意してある. 例えば PowerPCTMには, この目的のために二つの命令, SYNCと EIEIO(Enforced In-order Execution of Input/Output)がある.

50 これは不思議な見方と思うかも知れないが, このように働くシステムが存在する. 例えば クレジットカード口座の国際交換は, 通常国単位で精算され, 異る国の間の請求は, 周期的に調整される. だから口座の残高は国が違えば違うのだ.

51 分散システムにおいてこの見方は Lampert(1978)が追求した. 彼は分散システムで, 事象の順を確定するのに使う「大域時計」を作るのに通信をどう使うかを示した.

[ 目次, 前節, 次節, 索引 ]