Die Klammer gibt keine Ressource frei, wenn sie sich im Thread befindet

8

Ich habe Probleme mit Haskell's bracket: Wenn das zweite Argument in einem gegabelten Thread (unter Verwendung forkFinally) ausgeführt wird bracket, wird die Berechnung, die Ressourcen freigibt, nicht ausgeführt, wenn das Programm endet.

Hier ist Code, der das Problem veranschaulicht (mir ist bewusst, dass ich in diesem speziellen Fall die Pufferung deaktivieren kann, um sofort in die Datei zu schreiben):

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

Die Berechnung, die Ressourcen freigibt (und in die Konsole schreibt), wird niemals aufgerufen:

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

Wenn Sie das Programm durch Entfernen des Forking-Codes zu einem einzelnen Thread machen, wird maindas Problem behoben, und das Dateihandle wird geschlossen, wenn Sie das Programm mit Ctrl+ beenden c:

main = writeToFile "first_file"

Wie stelle ich sicher, dass der Code zur Ressourcenfreigabe in bracketausgeführt wird, wenn mehrere Threads verwendet werden?

Matthias Braun
quelle

Antworten:

5

mit throwTo

Anscheinend wird für den mit forkFinallynie erstellten Thread nie eine Ausnahme ausgelöst, und daher wird der ressourcenfreisetzende Code von bracketnie ausgeführt.

Wir können dies beheben, indem wir dies manuell tun, indem wir 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!"
Matthias Braun
quelle
Wie @ChrisSmith hervorhebt, müssen Sie warten, bis der untergeordnete Thread abgeschlossen ist, oder es wird eine Race-Bedingung geben. (Sie können den Rennzustand sehen, indem Sie kurz threadDelayvor dem Drucken einen hinzufügen "Closing handle".)
KA Buhr
5

Die Hauptursache für das Problem ist, dass mainIhr Prozess beim Beenden einfach stirbt. Es wartet nicht auf andere Threads, die Sie erstellt haben, um fertig zu werden. In Ihrem ursprünglichen Code haben Sie einen Thread zum Schreiben in die Datei erstellt, der jedoch nicht beendet werden konnte.

Wenn Sie den Thread beenden möchten, ihn aber zum Aufräumen zwingen möchten, verwenden throwToSie ihn wie hier. Wenn der Thread beendet werden soll, müssen Sie darauf warten, bevor Sie mainzurückkehren. Siehe So zwingen Sie den Haupt-Thread, auf das Ende aller untergeordneten Threads in Haskell zu warten

Chris Smith
quelle
0

mit async

Herstellung getLineBlock auf unbestimmte Zeit der Haupt - Thread spielt nicht schön mit nohup: Es wird nicht mit

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

Alternativ zu getLineund throwTokönnen Sie die folgenden asyncFunktionen verwenden :

import           Control.Concurrent.Async       ( withAsync, wait )

main = withAsync (writeToFile "first_file") wait

Dies ermöglicht das Ausführen des Programms mit nohup ./theProgram-exe &¹, beispielsweise auf einem Server über SSH .

async leuchtet auch, wenn mehrere Aufgaben gleichzeitig ausgeführt werden:

import           Control.Concurrent.Async       ( race_ )

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

Die Funktion race_führt zwei Aufgaben gleichzeitig aus und wartet, bis das erste Ergebnis eintrifft. writeToFileWenn wir nicht kündigen, wird es nie ein reguläres Ergebnis geben, aber wenn eine der Aufgaben eine Ausnahme auslöst, wird auch die andere abgebrochen. Dies ist beispielsweise nützlich, um einen HTTP- und einen HTTPS-Server gleichzeitig auszuführen.

Um das Programm sauber herunterzufahren und den Threads die Möglichkeit zu geben, Ressourcen bracketfreizugeben, sende ich ihm das SIGINT- Signal:

pkill --signal SIGINT theProgram-exe

Umgang mit SIGTERM

Um Threads auch ordnungsgemäß auf einem SIGTERM zu beenden , können wir einen Handler installieren , der das Signal abfängt:

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

Jetzt gibt unser Programm seine Ressourcen frei, bracketwenn es SIGTERM empfängt:

pkill theProgram-exe

Hier ist das Äquivalent für zwei gleichzeitige Aufgaben, die SIGTERM unterstützen:

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

Weitere Informationen zum Thema asynchrones Haskell finden Sie unter Parallele und gleichzeitige Programmierung in Haskell von Simon Marlow.


¹ Rufen Sie stack buildan, um eine ausführbare Datei zu erhalten, z..stack-work/dist/x86_64-linux-tinfo6/Cabal-2.4.0.1/build/theProgram-exe/theProgram-exe

Matthias Braun
quelle