haskell-jp / questions #104 at 2023-09-21 10:54:02 +0900

以下、timeout の抜粋です。
最近の timeout は、TimeManagerを使い「時間切れのどきだけ」スレッドが作られます(forkIOWithUnmask)。
(gitでlog を読むと高野さんのアイディアらしい。)
handleTimeoutで作られるこのスレッドは、どこにもブロックする箇所がないので、すぐに消滅しそうです。
しかし、cleanupTimeoutでわざわざkillThreadしています。
このkillThreadは必要なのでしょうか?

Haskell
timeout :: Int -> IO a -> IO (Maybe a)
timeout n f
    | n <  0    = fmap Just f
    | n == 0    = return Nothing
    | otherwise = do
        -- In the threaded RTS, we use the Timer Manager to delay the
        -- (fairly expensive) 'forkIO' call until the timeout has expired.
        --
        -- An additional thread is required for the actual delivery of
        -- the Timeout exception because killThread (or another throwTo)
        -- is the only way to reliably interrupt a throwTo in flight.
        pid <- myThreadId
        ex  <- fmap Timeout newUnique
        tm  <- getSystemTimerManager
        -- 'lock' synchronizes the timeout handler and the main thread:
        --  * the main thread can disable the handler by writing to 'lock';
        --  * the handler communicates the spawned thread's id through 'lock'.
        -- These two cases are mutually exclusive.
        lock <- newEmptyMVar
        let handleTimeout = do
                v <- isEmptyMVar lock
                when v $ void $ forkIOWithUnmask $ \unmask -> unmask $ do
                    v2 <- tryPutMVar lock =<< myThreadId
                    when v2 $ throwTo pid ex
            cleanupTimeout key = uninterruptibleMask_ $ do
                v <- tryPutMVar lock undefined
                if v then unregisterTimeout tm key
                     else takeMVar lock >>= killThread
        handleJust (\e -> if e == ex then Just () else Nothing)
                   (\_ -> return Nothing)
                   (bracket (registerTimeout tm n handleTimeout)
                            cleanupTimeout
                            (\_ -> fmap Just f))
なんとなく分かりました。

以下のレースを考えてみます。
(1) タイムアウトが発生し、ハンドラがtryPutMVarを実行した後、中断
(2) mainのアクションが完了し、cleanupTimeoutが走る。
vはFalseなので、takeMVar lock
= killThread が走る。
そして、handleJustを抜ける。
(3) ハンドラが、throwTo pid ex を実行。
(4) ex が main に届き、catch されない。

こういうことが起こりそうです。
しかし、「throwTo は同期」らしいです。
並行並列本に書いてありました。。。
なので、必ず相手にとどけてから戻ってきます。

killThread は throwTo なので、(2)の時点でハンドラは殺され、(3)は起きないそうです。
深い。
もう forkIO しなくて、TimeManager がthrowTo してもいいじゃんと思ったけど、action が FFI を含んでいると throwTo が戻らない可能性があり、TimeManager がブロックされるんだね。