割り込み可能な他言語呼び出し

 第24回では,FFIを使って呼び出した関数の動作がブロックされても,その関数を呼び出していない他のスレッドは止まらずに動作し続けるという「公平性の保証」について説明しました。しかし,この仕組みは,呼び出された外部関数が行う処理そのものを中断するための仕組みではありません。

 FFIを使って関数を呼び出しているスレッドを,killThreadやthrowToなどで非同期例外を送ることで終了させたくても,外部関数を使った処理が終わるまではそのスレッドを終わらせることはできません。どうしても外部関数を使った処理を中断させたければ,GHC 7.2.1で新しく提供された「割り込み可能な他言語呼び出し」(Interruptible foreign calls)を利用する必要があります(参考リンク)。

 割り込み可能な他言語呼び出しは,LANGUAGE指示文か-X*オプションでInterruptibleFFIを有効化し,foreign宣言での関数呼び出しにinterruptibleという修飾を付けることで利用できます。

{-# LANGUAGE InterruptibleFFI #-}

foreign import ccall interruptible
   "sleep" :: CUint -> IO CUint

 interruptibleという修飾を付けて呼び出された外部関数を利用しているスレッドに対して非同期例外が送られると,その外部関数に対して,処理を中断するためのエラーが送られます。ただし,実際に処理が中断されるには,エラーによる処理の中断にその外部関数が対応している必要があります。

 UNIX環境では,InterruptileFFIはpthread_kill関数を使ったSIGPIPEシグナル送出として実装されています。InterrupbleFFIを使った外部関数呼び出しに対して割り込みが発生した場合,呼び出された外部関数に対してEINTRが送られます。したがって,処理を中断するには,呼ばれる外部関数が「EINTRを受け取った場合に処理を中断する」ように実装されている必要があります。UNIX環境のシステムプログラミングでは,EINTRを常に検査することが良い習慣とされているので,OSのシステムコールや,システムコールを使う形で実装されている関数であれば,InterruptibleFFIを使って処理を中断できます。

 Windowsでは,Windows Vista以降で追加されたCancelSynchronousIO関数を使ってInterruptileFFIが実装されている点に注意する必要があります。Vistaよりも前のWindowsでは,CancelSynchronousIO関数が提供されていないため,InterrupbleFFIの仕組みは有効になりません。InterruptibleFFIを使った呼び出しは通常のsafeな外部関数呼び出しと同じになります。

 Vista以降のWindowsでは,CancelSynchronousIO関数によって外部関数に対してERROR_OPERATION_ABORTEDというエラーの通知が行われます。残念ながらERROR_OPERATION_ABORTEDはWindows XP以降で実装されたため,ERROR_OPERATION_ABORTEDエラーを受け取った場合に処理を中断するよう実装されている関数はあまり多くありません。そのため,CancelSynchronousIO関数を使って処理を中断できるのは,基本的にはWin32 APIで提供されるI/O処理の一部に制限されます。どの関数がCancelSynchronousIO関数による処理の中断に対応しているかは,MSDNなどのドキュメントを参照してください(参考リンク)。

 このように,InterruptibleFFIを使って中断できる処理には制限があります。呼び出したい外部関数がInterruptibleFFIによる中断に対応していない場合には,外部関数をラップするCコードを書いてからInterruptibleFFIを使って呼び出す必要があります。

 なお,第14回で説明したSystem.ProcessモジュールのwaitForProcess関数は,GHC 7.2.1以降ではInterruptibleFFIを使った中断可能な処理として実装されています。InterruptibleFFIを試してみるには,処理に時間が掛かるプロセスをSystem.Processで意図的に実行し,terminateProcess関数による処理の終了待ちを中断させるとよいでしょう(参考リンク)。

waitForProcess
  :: ProcessHandle
  -> IO ExitCode
waitForProcess ph = do
  p_ <- withProcessHandle ph $ \p_ -> return (p_,p_)
  case p_ of
    ClosedHandle e -> return e
    OpenHandle h  -> do
        ~ 略 ~
        alloca $ \pret -> do
          throwErrnoIfMinus1Retry_ "waitForProcess" (c_waitForProcess h pret)
          withProcessHandle ph $ \p_' ->
            case p_' of
              ClosedHandle e -> return (p_',e)
              OpenHandle ph' -> do
                ~ 略 ~
                return (ClosedHandle e, e)

~ 略 ~
foreign import ccall interruptible "waitForProcess" -- NB. safe - can block
  c_waitForProcess
        :: PHANDLE
        -> Ptr CInt
        -> IO CInt


著者紹介 shelarcy

 今回はParモナドの実装については説明しませんでした。Parモナドで提供される機能は,forkIO関数などの既存の並行Haskellの機能を単純にラップしたものではなく,Haskell製の独自スケジューラで実行される処理として実装されています。興味があればこうした実装を見てみるとよいでしょう(参考リンク1参考リンク2

 EvalモナドやParモナドのほかに,Intel CnC(Concurrent Collections)のHaskell移植版であるhaskell-cncパッケージでも,Parモナドをもう少し複雑にしたような形の並列処理を提供しています。haskell-cncは,Parモナドとは少し異なる並列プログラミングのモデルを採用しているので,そうした部分も含めて興味があれば見てみると面白いでしょう(参考リンク1参考リンク2)。