Haskell による並列・並行プログラミング #12 Presentation Mode 推奨
前回までのあらすじ
9.1 非同期例外
ユーザーからの割り込みや、別スレッドからの割り込みなど
9.2 非同期例外のマスク
mask :: ((IO a -> IO a) -> IO b) -> IO b
mask に関数を渡すと、その関数の実行が終了するまで非同期例外の伝達が遅延される
takeMVar などの割り込み可能関数は mask 中でも割り込みが発生する
9.3 bracket
mask を使ってクリティカルセクションを守っている
9.4 チャネルに対する非同期例外の安全性
modifyMVar_, modifyMVar, withMVar, mask_ などが便利
9.5 タイムアウト
子スレッド(タイマー)によるタイムアウトと外部からの非同期例外を区別してハンドリングする例
9.6 非同期例外の捕捉
catch などの関数で非同期例外を捕捉したいとき、別の非同期例外が飛んできたらどうする?
catch を mask で包み、restore と対にすれば良い
code:haskell
mask $ \restore ->
restore action catch handler
実は、Haskellの例外ハンドラは暗黙的にマスクされている
暗黙のmaskの落とし穴
例外ハンドラから別の関数を末尾呼び出しすると、その関数は暗黙のmaskの内側に残ってしまう
悪い例: catch-mask.hs
実行すると、1回目のgetMaskingStateはUnmaskedを返すが、2回目ではMaskedInterruptibleになる
L14 の例外ハンドリング(MaskedInterruptible)中の loop 関数呼び出しが原因
良い例: catch-mask2.hs
Control.Exception.try を使って書く
例外ハンドラは try 内部に隠されたので、 mask 内部で再帰呼び出しされることが防がれた!
9.7 maskとforkIO
async 関数再訪
code:haskell
async :: IO a -> IO (Async a)
async action = do
m <- newEmptyMVar
t <- forkIO (do r <- try action; putMVar m r)
return (Async t m)
実は、try と putMVar の間に例外が発生すると、MVarは空のままになる
プログラムが Async の結果をwaitした場合にデッドロックしてしまう
mask で防げるのだが、 try の直前に例外が発生すると防ぎようがない
じゃあ forkIO も mask で包んじゃえばいいじゃん?
子プロセスってマスクされるものなの…?
forkIO の mask 対応
実は、folkIO は親スレッドのマスク状態を継承したスレッドを生成する
つまりこう書ける
code:haskell
async :: IO a -> IO (Async a)
async action = do
m <- newEmptyMVar
t <- mask $ \restore ->
forkIO (do r <- try (restore action); putMVar m r)
return (Async t m)
forkIO の一般化
「スレッドが完了した時に何らかのアクションを実行する」という形式は頻出
Control.Concurrent.forkFinally
code:haskell
forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId
forkFinally action fun =
mask $ \restore ->
forkIO (do r <- try (restore action); fun r)
forkFinally を使う
code:haskell
async :: IO a -> IO (Async a)
async action = do
m <- newEmptyMVar
t <- forkFinally action (putMVar m)
return (Async t m)
forkIO (x \`finally\` y) という箇所をみつけたら forkFinally x (\_ -> y) と書いたほうが良い
9.8 非同期例外に関して
ここまで、timeout や Chan などの複雑な抽象化を見てきた
Haskellでプログラミングする上でこのレベルで非同期例外を扱うことは稀である
IOを含まないHaskellコードは概ね安全
bracket のような非同期例外安全な抽象化を使おう
MVar を使うなら modifyMVar 族を使おう
複雑な場合
(リソースなど)状態に依存するアクションが連続する場合
一連のアクションを mask に包んで非同期例外をポーリングするようにしよう
その中で割り込み可能な操作を見つけて、割り込み可能な操作が発生させる例外を正しくハンドリングするようにしよう
MVar などの状態表現の代わりにソフトウェアトランザクショナルメモリ(STM)を使おう
複数の操作を合成して、不可分な単位を作ることができる(10章にて)
非同期例外は便利
スタックオーバーフローやユーザ割り込み(Ctrl+C)など多くの例外を非同期例外として扱える
サードパーティ製コードであっても割り込みはちゃんとできる
例外は外部関数呼び出し(15.3.1で議論)
Haskellではスレッドが何もできずにただ死ぬということはなく、後始末をする機会や例外ハンドラを走らせる機会がある
近年の非同期例外のトピック
safe-exception モジュール
syocy さんの記事、9.8節の楽観的な話と対比してやっぱり非同期例外がツラいという話をされている
10章 ソフトウェアトランザクショナルメモリ
担当: wado さん
STM
不可分に実行されるコード片
伝統的な問題
哲学者の食事問題
スレッドの起こし忘れバグ
マージ
(8章のダウンローダ)
Control.Concurrent.STM
ウィンドウズマネージャ
複数のスレッドを使う(ユーザーからの入力、ディスプレイ描画、プログラムからの要求)
Display
a display has some desktops
a desktop has some windows
可変なのでどこかにMVarを置くことになる
moveWindow (Desktop に MVar を置く場合)
移動中にデスクトップを取られるかもしれない問題
食事をする哲学者問題
解決策:正しい順番でMVarからの取り出しを行い、逆順で開放するのを要請する
面倒!エラーの温床になる!状態の順序付けが大変!
STMを使う(TVar)
TVar: トランザクション変数
STM モナドの中でSTMを用いたアクションを書き、最後にatomicallyでSTMアクションを実行
トランザクションが大きくなると性能低下を招く
atomically内の操作列はプログラムの他の部分からは不可分に見える
正しい順序・複数のMVarの取得といった問題が解決する
swapWindows
STM操作の合成可能性を示す
STM操作は通常は atomically に包まずに提供される(クライアントが atomically する)
Tips
なぜSTMとIOは異なるモナドなのか?
トランザクションのエフェクトをロールバックできる
TVar上の操作に限定される
retry
ブロッキング
条件が真になるのを待つ
リソースを待つ
MVarを作ってみる
中身がある状態・空状態
Maybe a を含むTVarでモデル化
takeTMVar/putTMVar
ブロックが必要になった時点で retry
retryは意味なく繰り返し動くわけではなく、TVarが変化したタイミングで再走するらしい
10.3 変更されるまでのブロッキング
フォーカスを持つデスクトップの例
現在どのデスクトップにフォーカスがあるかという問題は、描画スレッドとユーザー入力処理スレッドが共有する
フォーカスをTVarで表す
ウィンドウに変更がなければ待ちたい(retry)
再描画のタイミング
focusの変更
現在のデスクトップへの変更
ここまでの疑問点
retryのためにTVerの変化を見ているけど、その部分はどうやって実現してるのだろうか
ポーリング?
次回 10.4 STMを使ったマージ
担当: wado さん
11章 並行性の高水準な抽象化
担当: maton