専用の関数で結合スレッドを確認

 結合スレッドを利用するための機能はforkOSだけではありません。他の機能も見ていきましょう。

 Control.Concurrentモジュールでは,使用している実行時システムが結合スレッドに対応しているかを確かめる「rtsSupportsBoundThreads」という関数と,使用中のスレッドが結合スレッドであるかどうかを確かめる「isCurrentThreadBound」という関数を提供しています(Haskell'ではrtsSupportsBoundThreadsはsupportsBoundThreadsに変更される予定です,参考リンク)。

Prelude Control.Concurrent> :t rtsSupportsBoundThreads
rtsSupportsBoundThreads :: Bool
Prelude Control.Concurrent> rtsSupportsBoundThreads
True
Prelude Control.Concurrent> :t isCurrentThreadBound
isCurrentThreadBound :: IO Bool

{-# LANGUAGE ForeignFunctionInterface #-}
module RTS where
import Control.Concurrent
import Control.Concurrent.STM

main = do
    m <- newEmptyTMVarIO

    printThreadState
    forkIO $ do
        printThreadState
        printThreadState_reimported
        atomically $ putTMVar m ()
    forkOS printThreadState

    atomically $ takeTMVar m

printThreadState :: IO ()
printThreadState = do
    f <- isCurrentThreadBound
    print f


foreign export ccall printThreadState :: IO ()
foreign import ccall "printThreadState" printThreadState_reimported :: IO ()

$ ghc --make RTS.hs -main-is RTS -threaded
[1 of 1] Compiling RTS              ( RTS.hs, RTS.o )
Linking RTS.exe ...

$ ./RTS
True
False
Ture
True

 forkIOで作成したスレッド以外,メイン・スレッドを含めすべて結合スレッドになっているのがわかります。面白いことに,「printThreadState」を一度外部に公開してから再度呼び出した「printThreadState_reimported」は,forkIOの内部で呼ばれているにもかかわらず,結合スレッドとして扱われています。このように,foreign exportで外部に公開された関数は結合スレッドになります。同じことは,exportとimportを使って行う処理を隠ぺいしたwrapperにも言えます。

 ところで,「いちいちrtsSupportsBoundThreadsやisCurrentThreadBoundを使って,結合スレッドとして利用できるかどうかを確認する」のは面倒ではないでしょうか? こうした要求に応えるため,Control.Concurrentのモジュールでは結合スレッドや非結合スレッドでの実行を保証するための関数「runInBoundThread」と「runInUnboundThread」を用意しています。

Prelude Control.Concurrent> :t runInBoundThread
runInBoundThread :: IO a -> IO a
Prelude Control.Concurrent> :t runInUnboundThread
runInUnboundThread :: IO a -> IO a

 runInBoundThreadを結合スレッドではないスレッドで使用した場合,処理系が対応していれば,結合スレッドを作成してプログラムをその上で実行します。こうすることで,結合スレッド上での実行が保証されるのです。逆に,runInUnboundThreadを結合スレッドで使用した場合,結合スレッドではないスレッドを新たに作成してその上でプログラムを実行します。

 以下にGHCでの実装を載せておきます。

foreign export ccall forkOS_entry
    :: StablePtr (IO ()) -> IO ()

foreign import ccall "forkOS_entry" forkOS_entry_reimported
    :: StablePtr (IO ()) -> IO ()

forkOS_entry stableAction = do
        action <- deRefStablePtr stableAction
        action

~ 略 ~

runInBoundThread :: IO a -> IO a

runInBoundThread action
    | rtsSupportsBoundThreads = do
        bound <- isCurrentThreadBound
        if bound
            then action
            else do
                ref <- newIORef undefined
                let action_plus = Exception.try action >>= writeIORef ref
                resultOrException <-
                    bracket (newStablePtr action_plus)
                            freeStablePtr
                            (\cEntry -> forkOS_entry_reimported cEntry >> readIORef ref)
                case resultOrException of
                    Left exception -> Exception.throw exception
                    Right result -> return result
    | otherwise = failNonThreaded

~ 略 ~

runInUnboundThread :: IO a -> IO a

runInUnboundThread action = do
    bound <- isCurrentThreadBound
    if bound
        then do
            mv <- newEmptyMVar
            forkIO (Exception.try action >>= putMVar mv)
            takeMVar mv >>= \either -> case either of
                Left exception -> Exception.throw exception
                Right result -> return result
        else action

 GHCではrunInBoundThread内での結合スレッドの作成に,forkOSではなくforeign exportを使っています。このため,誤ってガーベジ・コレクション(GC)されないように,前回の第23回で紹介したStablePtrを使用して,IOアクションをGCの対象から外しています。