非同期例外のブロックを制御するAPIの変更

 第26回では,非同期例外を制御するためのAPIとして,非同期例外をブロックするblock関数と非同期例外のブロックを解除するunblock関数を紹介しました。しかし,blockとunblockを使うブロック制御には問題があることが後になって判明しました。

 GHC 6.12.3までのbracket関数やfinally関数では,非同期例外を考慮した例外処理のためにblockとunblockをセットで呼んで使用しています。

bracket
        :: IO a         -- ^ computation to run first (\"acquire resource\")
        -> (a -> IO b)  -- ^ computation to run last (\"release resource\")
        -> (a -> IO c)  -- ^ computation to run in-between
        -> IO c         -- returns the value from the in-between computation
bracket before after thing =
  block (do
    a <- before
    r <- unblock (thing a) `onException` after a
    after a
    return r
 )

~ 略 ~

finally :: IO a         -- ^ computation to run first
        -> IO b         -- ^ computation to run afterward (even if an exception
                        -- was raised)
        -> IO a         -- returns the value from the first computation
a `finally` sequel =
  block (do
    r <- unblock a `onException` sequel
    sequel
    return r
  )

 この定義は,非同期例外をブロックしていない状況下では適切ですが,非同期例外をブロックしている場合には問題があります。なぜなら,非同期例外をブロックしている状況下でbracketやfinallyを呼び出すと,これらの関数の内部でunblock関数が呼び出され,プログラマの意図に反して非同期例外のブロックが解除されてしまうからです。意図しない非同期例外のブロック解除を防ぐには,非同期例外をブロックした処理の中で,bracketやfinallyなどのような「非同期例外をブロックしているかどうかを判断せずにblockとunblockを利用する関数」を使うことはできません。このように,blockとunblockを使うブロック制御には,関数のモジュール性を損なってしまう可能性があります(参考リンク)。

 この問題を解決するためGHC 7.0.1では,プログラマが非同期例外を考慮した例外処理を記述する際にブロックが解除されないようにする新しいインタフェースが提供されることになりました。Control.Exceptionモジュールで新しく提供されるmask関数では,非同期例外がブロックされていない場合に限り,非同期例外のブロックと非同期例外のブロックの解除をセットで行います(参考リンク)。

mask :: ((forall a. IO a -> IO a) -> IO b) -> IO b
mask io = do
  b <- getMaskingState
  case b of
    Unmasked -> block $ io unblock
    _        -> io id

 getMaskingStateは,実行中のアクションが非同期例外をブロックしているかどうかを確認するための関数です。maskと同じく,Control.Exceptionモジュールで提供されます。

getMaskingState :: IO MaskingState

-- | Describes the behaviour of a thread when an asynchronous
-- exception is received.
data MaskingState
  = Unmasked -- ^ asynchronous exceptions are unmasked (the normal state)
  | MaskedInterruptible 
      -- ^ the state during 'mask': asynchronous exceptions are masked, but blocking operations may still be interrupted
  | MaskedUninterruptible
      -- ^ the state during 'uninterruptibleMask': asynchronous exceptions are masked, and blocking operations may not be interrupted
 deriving (Eq,Show)

 非同期例外をブロックしないモードであることを示す値はUnmasked,非同期例外をブロックするモードであることを示す値はMaskedInterruptibleとMaskedUninterruptibleです。

 maskでは,getMaskingStateから取得した状態を利用し,非同期例外をブロックしていない場合と,非同期例外をブロックしている場合でそれぞれ別の処理を記述しています。非同期例外をブロックしていない場合には,「新たに非同期例外をブロックし,非同期例外のブロックを解除した部分で例外処理を行う」という従来通りの例外処理を行います。一方,すでに非同期例外をブロックしている場合には,非同期例外をブロックしたままで例外処理を行います。このように,maskでは非同期例外がすでにブロックされている場合には,非同期例外のブロックは解除されません。このため,maskを使う処理ではモジュール性が損なわれません。

 では,maskを使うbracket関数とfinally関数の定義を見てみましょう。

bracket
        :: IO a         -- ^ computation to run first (\"acquire resource\")
        -> (a -> IO b)  -- ^ computation to run last (\"release resource\")
        -> (a -> IO c)  -- ^ computation to run in-between
        -> IO c         -- returns the value from the in-between computation
bracket before after thing =
  mask $ \restore -> do
    a <- before
    r <- restore (thing a) `onException` after a
    _ <- after a
    return r

~ 略 ~

finally :: IO a         -- ^ computation to run first
        -> IO b         -- ^ computation to run afterward (even if an exception
                        -- was raised)
        -> IO a         -- returns the value from the first computation
a `finally` sequel =
  mask $ \restore -> do
    r <- restore a `onException` sequel
    _ <- sequel
    return r

 ラムダ式に引数で与えられるrestore変数には,非同期例外をブロックしていない場合にはunblock,非同期例外がブロックされている場合にはidが,mask関数により束縛されます。このように,非同期例外をブロックしているかどうかに応じて適切な処理が自動的に振り分けられるため,mask関数を使って例外処理を書くプログラマーは非同期例外をブロックしているかどうかを気にする必要はありません。blockとunblockがmaskとrestoreに変わったことを除けば,上に示したGHC 6.12.3までの定義とほぼ同じ形になっているのがわかると思います。

 ただし,maskは例外処理を目的とした関数です。block関数のように,例外処理を行わずに単に非同期例外をブロックするだけの関数が欲しいこともあります。このために提供されているのがmask_関数です。

-- | Like 'mask', but does not pass a @restore@ action to the argument.
mask_ :: IO a -> IO a
mask_ io = mask $ \_ -> io

 また,GHC 7.0.1には入りませんでしたが,unblock関数のように非同期例外のブロックを解除する関数として,allowInterrupt関数が今後追加される予定になっています(参考リンク1参考リンク2)。

-- | When invoked inside 'mask', this function allows a blocked
-- asynchronous exception to be raised, if one exists.  It is
-- equivalent to performing an interruptible operation (see
-- #interruptible#), but does not involve any actual blocking.
--
-- When called outside 'mask', or inside 'uninterruptibleMask', this
-- function has no effect.
allowInterrupt :: IO ()
allowInterrupt = unsafeUnmask $ return ()

 allowInterruptはunblock関数を使って以下のように定義できます。GHCに正式に入るまでの間は,こうして定義した関数を代わりに使ってください。

allowInterrupt :: IO ()
allowInterrupt = unblock $ return ()

 allowInterrupt関数は,mask関数による非同期例外のブロックを意図的に解除し,非同期例外による処理への割り込みと非同期例外に対する例外処理を可能にするものです。allowInterruptは,意図しないブロック解除を防ぐため,blockやunblockのような使い方ができないよう,I/Oアクションを引数として取らない,単なる「IO ()」型の関数として定義されています。

 ただし,allowInterruptの定義の工夫だけでは意図しないブロック解除を防ぎきれません。allowInterrupt関数を使う処理には「bracketUnmasked」や「bracketWithUnmask」といった名前を付け,非同期例外のブロック解除を行う意図を明確にしておくべきです。よく利用する処理であれば,ブロック解除を行わない処理とブロック解除を行う処理の二つのバージョンを用意しておくべきでしょう。

 maskによる非同期例外のブロックの内側でも,allowInterruptを使って非同期例外に対するブロックを解除すれば,非同期例外に対する処理を行えます。しかし,unblockとは異なり,maskによるブロックを解除してI/O処理を行うことはできません。このため「親スレッドのモードを引き継ぐことなく,ブロックを行わないモードで新しいスレッドを作成する関数」は実現できません。そこでGHC 7.0.1から提供されているのが,Control.ConcurrentモジュールのforkIOUnmasked関数です。

 forkIOは,mask関数やmask_関数などによるモードの変更(MaskedInterruptibleまたはMaskedUninterruptible)を引き継ぎます。これに対しforkIOunmaskedでは,スレッドは非同期例外をブロックしないUnmaskedモードになります。このことは,以下のようにgetMaskingStateを使って現在のモードを出力することで確認できます(GHCiのメインスレッドと新しく作成したスレッドが同時に結果を出力するため,実際には「状態の出力」と「Prelude Control.Concurrent Control.Exception>」が入り混じって表示されます。以下の例はわかりやすいように整形しています)。

Prelude Control.Concurrent Control.Exception> _ <- forkIO $ getMaskingState >>= print
Unmasked
Prelude Control.Concurrent Control.Exception> _ <- mask_ $ forkIO  $ getMaskingState >>= print
MaskedInterruptible
Prelude Control.Concurrent Control.Exception> _ <- forkIOUnmasked  $ getMaskingState >>= print
Unmasked
Prelude Control.Concurrent Control.Exception> _ <- mask_ $ forkIOUnmasked  $ getMaskingState >>= print
Unmasked

 このように非同期例外のブロック制御の仕組みが変更されたことにより,従来のblockとunblockは非推奨になります。GHC 7.0.1あるいは7.0.2を含む予定のHaskell Platform 2011.1.0.0は,2011年1月中にリリースされる予定です。すでにblockやunblockを使って非同期例外を処理するコードを書いている場合には,mask関数やallowInterrupt関数などを使ったコードに書き換えてください(参考リンク)。

 なお,これまで非同期例外のブロックでは,「MVarによるデッドロックの発生」といった緊急時には,たとえmaskやblockでブロックされていても,ブロックを行わず必ず例外を発生させていました(参考リンク)。

 しかし,OSのリソースにアクセスしている最中などに例外が発生してしまうと,後処理が行われなくなるという問題が生じます。そこで「MVarによるデッドロックの発生」などの緊急時でも例外による割り込みを発生させないMaskedUninterruptibleモードや uninterruptibleMask関数が提供されています。

 uninterruptibleMaskは強力ですが,緊急を要する状況が新たに発生しても例外として伝えられなくなるので注意してください。uninterruptibleMaskを利用するのは,ブロックした緊急状況にすぐに対処できる短時間の処理に限定すべきでしょう。例えば,後処理そのものではなく,「後処理を行うことを決定するだけの処理」などです。

 非同期例外処理のAPIにはまだまだ不安定な部分があります(参考リンク参考リンク)。非同期例外を使っている方は,GHCやHaskell Platformの新バージョンがリリースされた際には,非同期例外のAPIが変更されていないかどうか注意してください。


著者紹介 shelarcy

 今回はprogressionというパッケージを紹介しましたが,ほかにもcriterionやprogressionで測定した複数の結果をグラフとして表示するツールがあります。興味があれば,HackageDBで提供されているbarchartパッケージなどを試してみるとよいでしょう(参考リンク)。

 Haskellを使って測定した結果だからといって,Haskell製のライブラリやツールにこだわる必要はありません。Excelなどの表計算ソフトやRなどの統計処理ソフトに慣れている方は,criterionやprogressionが出力するCSVファイルをこれらのソフトに読み込ませてグラフを表示してみるとよいでしょう。