ロックとアンロック

並行プログラミングでは、共有リソースに対する安全なロックとアンロックは本質的で意欲的な課題です。 この章では、線形型がこの課題を解決するのに有効であることを具体的に説明します。

はじめに、次のような線形抽象型 shared を導入しましょう:

absvtype shared(a:vtype) = ptr

(なんらかのリソースに対する) 観型 VT が与えられたとき、型 shared(VT) の値は、本質的に、型 VT のリソースとなんらかのロックを含んだボックス化レコードです。 リソースを共有リソースにするために、次のような関数 shared_make が呼ばれます。

fun shared_make{a:vtype}(x: a): shared(a)

shared(VT) それ自身が線形であることに注意してください。 実装では、スピンロックで保護された線形共有リソースにはリファレンスカウントが内在しています。 関数 shared_refshared_unref は共有リソース内のリファレンスカウントを増減させます:

fun shared_ref{a:vtype}(!shared(a)): shared(a) fun shared_unref{a:vtype}(shared(a)): Option_vt(a)

もし共有リソースのリファレンスカウントが 1 であったら、その共有リソースへの shared_unref 呼び出しはそれらの表現で使われていたメモリを解放し、その共有リソースに保存されていたリソースを返します。

関数 shared_lock は与えられた共有リソースからリソースを獲得し、関数 shared_unlock はその反対です:

// absview locked_v // fun shared_lock{a:vtype}(!shared(a)): (locked_v | a) fun shared_unlock{a:vtype}(locked_v | !shared(a), x: a): void //

抽象観 locked_v が線形証明として導入されていることに注意してください。この線形証明は共有リソースは獲得の後に解放しなければならないことを、プログラマに気付かせます。

予想できることですが共有リソースは、スピンロック、リファレンスカウントと (保管されたリソースを差す) ポインタから成る、ボックス化タプルとして実装できます:

// datavtype shared_ (a:vtype) = SHARED of (spin1_vt(*lock*), int(*count*), ptr) // assume shared = shared_ //

spin1_vt は線形スピンロックを表わすことに注意してください。関数 shared_ref は次のように実装されます:

implement shared_ref {a}(sh) = let // val+@SHARED (spin, count, _) = sh // val spin = // for temp. use unsafe_spin_vt2t(spin) // val (pf | ()) = spin_lock(spin) val c0 = count val () = count := c0 + 1 val ((*void*)) = spin_unlock(pf | spin) prval ((*void*)) = fold@sh // in $UN.castvwtp1{shared(a)}(sh) end // end of [shared_ref]

この実装が安全でないキャストをいくつか使っていることは明白です。そのようなキャストを使わない実装は可能だとしてもとても複雑になるでしょう。 そのとき c0count に保管された整数の間に束縛を作る前にスピンロックを獲得しなければなりません。 そうでなければレースコーンディションの原因になります。 関数 shared_unref は次のように実装されます:

implement shared_unref {a}(sh) = let // val+@SHARED (spin, count, _) = sh // val spin = // for temp. use unsafe_spin_vt2t(spin) // val (pf | ()) = spin_lock(spin) val c0 = count val () = count := c0 - 1 val ((*void*)) = spin_unlock(pf | spin) prval ((*void*)) = fold@sh // in // if c0 <= 1 then let val+~SHARED(spin, _, x) = sh val ((*freed*)) = spin_vt_destroy(spin) in Some_vt($UN.castvwtp0{a}(x)) end // end of [then] else let prval () = $UN.cast2void(sh) in None_vt() end // end of [else] // end // end of [shared_unref]

リファレンスカウントが 1 である場合、共有リソースが解放され、その中のスピンロックが破棄され、そして共有リソースの中にあったリソースが返ります。

関数 shared_lockshared_unlock は次のように実装されます:

implement shared_lock {a}(sh) = let // val+@SHARED(spin, _, x) = sh // val spin = unsafe_spin_vt2t(spin) // val (pf | ()) = spin_lock(spin) // val x0 = $UN.castvwtp0{a}(x) // prval () = fold@sh // in ($UN.castview0(pf) | x0) end // end of [shared_lock]

implement shared_unlock {a}(pf | sh, x0) = let // val+@SHARED(spin, _, x) = sh // val spin = unsafe_spin_vt2t(spin) // val () = x := $UN.castvwtp0{ptr}(x0) // val () = spin_unlock($UN.castview0(pf) | spin) // prval () = fold@sh // in // nothing end // end of [shared_unlock]

shared_lock において、値 x の中身はスピンロック獲得後に読み出されていることに注目してください。 これは重要な点で、さもなくばレースコンディションを引き起こします。 shared_unlock においては、獲得されたスピンロックを解放する前に、値 x の中身を更新しています。

この章で紹介したコード全体を含む shared_vt.dats ファイルはオンラインから入手できます。 また、このファイルはロックを保持した動作が: スレッド0が動作し; スレッド1が動作し; スレッド2が動作し; スレッド0が動作し; スレッド1が動作し; スレッド2が動作し; というように動く3つのスレッドの実装も含んでいます。