けいぞうのメモ帳

言語設計のお勉強

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> 
  1. ghciをoption-fbrea-n-exceptionつきで起動する。
  2. 対象のプログラムを読み込む
  3. :trace コマンドでmainを走らせる
  4. SomeExceptionが投げられプログラムが中断する
  5. :backコマンドでスタックトレースが行われる

スタックトレースによってどの変数でBlockedしたのか、それがどのファイル名、どの行数で起こったのかなどがわかる。

コードベースで対応する方法はgistにあげておく ref:

handleAsyncExcepOnMVar.hs · GitHub

これは行数、ファイル名が得られないため望ましくない。C言語ライクなFILE, LINEを出力するライブラリを用いて 対応することもあるが、実行時の行数が得られないため、無意味である。