MVarが"thread blocked indefinitely in an MVar operation"する時のデバッグ方法
haskellのmutexであるMVarがデッドロックを起こしたときのデバッグ方法を解説する。 そもそもMVarはhaskellにおけるもっともプリミティブな非同期共有の方法であり、 必要がないならばasyncやstmなどの高度な表現を用いることが推奨される。
環境
- MAC OS X
- Version 1.5.1 x86_64 hpack-0.17.1
- The Glorious Glasgow Haskell Compilation System, version 8.0.1
問題
haskellにおける非同期処理では、データをやり取りする方法がいくつかある。 そのうちのもっともプリミティブなものにMVarがある。
MVarには一つだけデータが入る。 そして、データが入っている間は入る操作を他の箇所から行うことが出来ずスレッドはブロックする。 また、データが存在しない間は読み出す操作を他の箇所から行うことは出来ずスレッドはブロックする。
正しく動くコードの例
module Main where import Control.Concurrent import Control.Concurrent.MVar main :: IO () main = run run :: IO () run = do v <- newEmptyMVar forkIO v $ \v -> do writeMVar v 1 d <- readMVar v print d return ()
forkIO
でスレッドを生成し、v :: MVar Int
を経由して数字1をスレッドから受け取っている。
もし、このコードが以下のようだった場合デッドロックが起こる。
module Main import Control.Concurrent import Control.Concurrent.MVar main :: IO () main = do v <- newMVar d <- readMVar v return ()
この時に表題の"thread blocked indefinitely in an MVar operation"がstdoutに出力されてプログラムは終了する。
対策
この時、GHCのruntimeはBlockedIndefinitelyOnMVar
という非同期例外を投げている。
よってこの例外を主眼において操作を行うことでデバッグ情報を取得することが出来る。
方法は2つあり、
- コードでControl.Exception.catch
を用いてBlockedIndefinitelyOnMVar
を捕まえてレポートする
- ghciでスタックトレースを行う
ことである。僕はghciでスタックトレースを取る方法を推奨する。 ref: https://downloads.haskell.org/~ghc/6.12.1/docs/html/users_guide/ghci-debugger.html#ghci-debugger-exceptions
ターミナルでの操作を以下のように行う。
$ stack ghci --ghci-options -fbreak-on-exception ghci> :l program.hs ghci> :trace main Stopped in <exception thrown>, <unknown> [<unknown>] *Main> :back Logged breakpoint at program.hs:11:3-12 _result :: IO GHC.Prim.Any v :: MVar GHC.Prim.Any [-1: program.hs:11:3-12] *Main>
- ghciをoption
-fbrea-n-exception
つきで起動する。 - 対象のプログラムを読み込む
- :trace コマンドでmainを走らせる
- SomeExceptionが投げられプログラムが中断する
- :backコマンドでスタックトレースが行われる
スタックトレースによってどの変数でBlockedしたのか、それがどのファイル名、どの行数で起こったのかなどがわかる。
コードベースで対応する方法はgistにあげておく ref:
handleAsyncExcepOnMVar.hs · GitHub
これは行数、ファイル名が得られないため望ましくない。C言語ライクなFILE, LINEを出力するライブラリを用いて 対応することもあるが、実行時の行数が得られないため、無意味である。