ブラケットはスレッド内でリソースを解放しません


8

Haskellで問題が発生しbracketています。フォークされたスレッド内で(forkFinallybracketの2番目の引数を使用して実行すると、プログラムの終了時にリソースを解放する計算が実行されません。

問題を示すコードは次のとおりです(この特定のケースでは、バッファリングを無効にしてファイルにすぐに書き込むことができることを知っています)。

import           System.IO
import           Control.Exception              ( bracket
                                                , throwTo
                                                )

import           Control.Concurrent             ( forkFinally
                                                , threadDelay
                                                )
main = do
  threadId <- forkFinally
    (writeToFile "first_file")
    (\ex -> putStrLn $ "Exception occurred: " ++ show ex)
  putStrLn "Press enter to exit"
  _ <- getLine
  putStrLn "Bye!"

writeToFile :: FilePath -> IO ()
writeToFile file = bracket
  (openFile file AppendMode)
  (\fileHandle -> do
    putStrLn $ "\nClosing handle " ++ show fileHandle
    hClose fileHandle
  )
  (\fileHandle -> mapM_ (addNrAndWait fileHandle) [1 ..])

addNrAndWait :: Handle -> Int -> IO ()
addNrAndWait fileHandle nr =
  let nrStr = show nr
  in  do
        putStrLn $ "Appending " ++ nrStr
        hPutStrLn fileHandle nrStr
        threadDelay 1000000

リソースを解放する(およびコンソールに書き込む)計算は呼び出されません。

putStrLn $ "\nClosing handle " ++ show fileHandle
hClose fileHandle

フォークコードを削除してプログラムをシングルスレッドにするmainと、問題がなくなり、プログラムをCtrl+で終了するとファイルハンドルが閉じますc

main = writeToFile "first_file"

bracket複数のスレッドを使用しているときに、リソース解放コードが確実に実行されるようにするにはどうすればよいですか?

回答:


5

を使用して throwTo

どうやらで作成されたスレッドforkFinallyは例外がスローされないため、リソースを解放するコードbracketが実行されることはありません。

これを手動で行うことでこれを修正できますthrowTo threadId ThreadKilled

import           Control.Exception              ( bracket
                                                , throwTo
                                                , AsyncException(ThreadKilled)
                                                )

import           Control.Concurrent             ( forkFinally
                                                , threadDelay
                                                )
main = do
  threadId <- forkFinally
    (writeToFile "first_file")
    (\ex -> putStrLn $ "Exception occurred: " ++ show ex)
  putStrLn "Press enter to exit"
  _ <- getLine
  throwTo threadId ThreadKilled
  putStrLn "Bye!"

@ChrisSmithが指摘するように、子スレッドが完了するのを待つ必要があります。そうしないと、競合状態が発生します。(threadDelay印刷する前にショートを追加することで競合状態を確認できます"Closing handle"。)
KA Buhr

5

ここでの問題の根本的な原因は、main終了時にプロセスが停止することです。作成した他のスレッドが完了するまで待機しません。したがって、元のコードでは、ファイルに書き込むためのスレッドを作成しましたが、完了を許可されていませんでした。

スレッドを強制終了したいが強制的にクリーンアップするthrowTo場合は、ここで行ったように使用します。スレッドを終了させたい場合は、main戻る前にそれを待つ必要があります。メインスレッドがすべての子スレッドがHaskellで完了するのを待機させる方法を参照してください。


0

を使用して async

作るgetLineブロックをメインスレッドが無期限で素敵を再生しませんnohup:それはで失敗します

<stdin>: hGetLine: invalid argument (Bad file descriptor)

getLineおよびの代わりに、の関数をthrowTo使用できます。async

import           Control.Concurrent.Async       ( withAsync, wait )

main = withAsync (writeToFile "first_file") wait

これによりnohup ./theProgram-exe &、たとえばSSH経由のサーバー上で、 ¹を使用してプログラムを実行できるようになります

async 複数のタスクを同時に実行するときにも光ります:

import           Control.Concurrent.Async       ( race_ )

main = race_ (writeToFile "first_file") (writeToFile "second_file")

関数race_は2つのタスクを同時に実行し、最初の結果が到着するまで待機します。終了しwriteToFileないと、通常の結果は得られませんが、タスクの1つが例外をスローすると、他のタスクもキャンセルされます。これは、たとえば、HTTPサーバーとHTTPSサーバーを同時に実行する場合に役立ちます。

プログラムをきれいにシャットダウンするには—スレッドにリソースを解放する機会を与えるbracket—私はそれにSIGINTシグナルを送ります。

pkill --signal SIGINT theProgram-exe

SIGTERMの処理

SIGTERMでスレッドを正常に終了するために、シグナルをキャッチするハンドラーインストールできます。

import           Control.Concurrent.Async       ( withAsync
                                                , wait
                                                , cancel
                                                , Async
                                                )
import           System.Posix.Signals

main = withAsync
  (writeToFile "first_file")
  (\asy -> do
    cancelOnSigTerm asy
    wait asy
  )

cancelOnSigTerm :: Async a -> IO Handler
cancelOnSigTerm asy = installHandler
  sigTERM
  (Catch $ do
    putStrLn "Caught SIGTERM"
    -- Throws an AsyncCancelled excepion to the forked thread, allowing
    -- it to release resources via bracket
    cancel asy
  )
  Nothing

これで、プログラムはbracketSIGTERMを受け取ったときにリソースを解放します。

pkill theProgram-exe

SIGTERMをサポートする2つの並行タスクに相当するものを次に示します。

import           Control.Concurrent.Async       ( withAsync
                                                , wait
                                                , cancel
                                                , Async
                                                , waitEither_
                                                )
import           System.Posix.Signals

main = raceWith_ cancelOnSigTerm
                 (writeToFile "first_file")
                 (writeToFile "second_file")

raceWith_ :: (Async a -> IO b) -> IO a -> IO a -> IO ()
raceWith_ f left right = withAsync left $ \a -> withAsync right $ \b -> do
  f a
  f b
  waitEither_ a b

非同期のHaskellのトピックの詳細については、Simon MarlowによるHaskellのParallel and Concurrent Programmingをご覧ください。


¹ たとえば、実行可能ファイルを取得するために呼び出すstack build.stack-work/dist/x86_64-linux-tinfo6/Cabal-2.4.0.1/build/theProgram-exe/theProgram-exe

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.