SICPの3.4 Concurrency: Time Is of the EssenceからExercise 3.42までバリバリ読み解きます。
いつもの高尚な前振りです。
という訳で並列処理に纏わる話が始まるようです。
銀行口座への預け入れと引き出しを例題にして並列処理×副作用=破滅になる様子がよく分かるようになってます。
という訳で本文を読み終えたとして早速問題を解きましょう。
Suppose that Peter, Paul, and Mary share a joint bank account that initially
contains $100. Concurrently, Peter deposits $10, Paul withdraws $20, and Mary
withdraws half the money in the account, by executing the following commands:
(set! balance (+ balance 10))
(set! balance (- balance 20))
(set! balance (- balance (/ balance 2)))
a. List all the different possible values for balance after these three
transactions have been completed, assuming that the banking system forces the
three processes to run sequentially in some order.
b. What are some other values that could be produced if the system allows the
processes to be interleaved? Draw timing diagrams like the one in figure 3.29
to explain how these values can occur.
aは全て組み合わせを試せば良いので簡単ですね。
という訳で取り得る値は$35、$40、$45および$50の4通りです。
bは……流石に全ての組み合わせを列挙するのは現実的ではないので、
一つだけ例を挙げることにしましょう:
| Bank Peter Paul Mary
|
| ($100)
| |||
| ||`---------------------------------------------------------.
| |`----------------------------------. |
| `-----------. | |
| | | |
| v | |
| [Access balance: $100] v |
| | [Access balance: $100] v
| v | [Access balance: $100]
| [new value: 100-10=90] v |
| | [new value: 100-20=80] v
| v | [new value: 100/2=50]
| [set! balance to $90] v |
| | [set! balance to $80] v
| ($90)<-------' | [set! balance to $50]
| | |
| ($50)<-------------------------------+-----------------------'
| |
| ($80)<-------------------------------'
v
time
こんな感じで$80が発生し得ますね。
ただ、Maryの分の新残高計算処理は(処理系の引数評価順序にも依るけれど)
balance
参照balance
参照のステップに分解できるので、
二度の balance
参照の合間に balance
が書き変わる例を作った方が良かったかな……
並列に実行されるプロセスに何の制約もなかったとしたら考慮すべき組み合わせが爆発的に増えてまともに動くものなんて作れやしない……
ということで制約の一種としてserializerを導入して実行を制御しようという話だそうです。
Which of the five possibilities in the parallel execution shown above remain
if we instead serialize execution as follows:
(define x 10)
(define s (make-serializer))
(parallel-execute (lambda () (set! x ((s (lambda () (* x x))))))
(s (lambda () (set! x (+ x 1)))))
このコードのうち同時実行されない部分は以下の通りです:
(\* x x)
(set! x Aの結果)
(set! x (+ x 1))
可能な組み合わせは以下の通りです:
よって取り得る値は100、101および121になります。
……と思ったのですが、よくよく考えるとBは s
でserializeされてないですね。
だから以下のコードの断片について考える必要があります。
(\* x x)
(set! x Aの結果)
(Aの後にしか来ない)(+ x 1)
(set! x P1の結果)
(P1の後にしか来ない/P1-P2間にAは割り込めない)可能な組み合わせは以下の通りです:
よって取り得る値は100、101および121になります。
あれ? 結局同じか……
Give all possible values of x that can result from executing
(define x 10)
(parallel-execute (lambda () (set! x (* x x)))
(lambda () (set! x (* x x x))))
Which of these possibilities remain if we instead use serialized procedures:
(define x 10)
(define s (make-serializer))
(parallel-execute (s (lambda () (set! x (* x x))))
(s (lambda () (set! x (* x x x)))))
まず未serializeの場合についてです。
最初の lambda
を P1、二番目の lambda
を P2 と呼ぶことにします。
コードの実行過程を以下のステップに分解して考えましょう:
考えられる実行順序は以下の通りです:
という訳で取り得る値は以下のいずれかになります:
次にserializeされた場合についてですが、
今度はP1とP2のステップが互い違いに実行されることは無いので、
考えられる実行順序は以下の通りです:
という訳で取り得る値は1000000のみになります。
Ben Bitdiddle worries that it would be better to implement the bank account
as follows (where the commented line has been changed):
(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)
;; continued on next page
(let ((protected (make-serializer)))
(define (dispatch m)
(cond ((eq? m 'withdraw) (protected withdraw))
((eq? m 'deposit) (protected deposit))
((eq? m 'balance)
((protected (lambda () balance)))) ; serialized
(else (error "Unknown request -- MAKE-ACCOUNT"
m))))
dispatch))
because allowing unserialized access to the bank balance can result in
anomalous behavior. Do you agree? Is there any scenario that demonstrates
Ben’s concern?
balance
の参照と変更はSchemeのコード上では1ステップのように見えますが、
処理系の内部処理レベルでは複数ステップになっているならBenの主張は正しいですね。
例えば
という謎のScheme処理系があるとしましょう。
この処理系で以下のコードを実行したとします:
(define a (make-account 1000))
(parallel-execute (lambda () ((a 'deposit) 234) ; P1
(lambda () (a 'balance))) ; P2
そうすると以下のような実行順序が考えられます:
balance
の上位2桁(10)を読む。balance
の値は1234になる。balance
の下位2桁(34)を読む。結果1034を返す。P2はP1による変更が行われる前と行われた後の環境の値を一部づつ組み合わせて結果としてしまうので、
あり得ない値が結果となってしまいます。
という訳でBenの主張が正しい世界も存在し得ます。
とはいえ、現実にはこんな処理系があるとは思えないので、
大抵の場合は単なる杞憂に過ぎないかも知れません。
Ben Bitdiddle suggests that it’s a waste of time to create a new serialized
procedure in response to every withdraw
and deposit
message. He says thatmake-account
could be changed so that the calls to protected are done
outside the dispatch procedure. That is, an account would return the same
serialized procedure (which was created at the same time as the account) each
time it is asked for a withdrawal procedure.
(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)))
Is this a safe change to make? In particular, is there any difference in what
concurrency is allowed by these two versions of make-account
?
セーフですね。
serializerとは何なのかの説明として
More precisely, serialization creates distinguished sets of procedures such
that only one execution of a procedure in each serialized set is permitted to
happen at a time.
つまり
ということです。
Benの変更をすると同一のserializeされた手続きを複数のプロセスが同時に実行しようとする状況が発生しますが、
上記の定義に従えばこういう状況になってもやはり1プロセスしか同時には実行されないですね。
3.4.2 Mechanisms for Controlling ConcurrencyのComplexity of using multiple shared resourcesをバリバリ読んでExercise 3.45まで解きます。