New concurrency tool: unfork

I recently released a new library called unfork, which makes it really easy to serialize actions used by concurrent threads. My primary motivation, which I think is pretty common, is that when concurrent threads print to stdout, it’s possible for the messages to be interleaved, and of course we wouldn’t want that to happen.

Usage example:

import Unfork

main :: IO ()
main =
    unforkAsyncIO_ putStrLn \putStrLn' ->
        _ -- Within this continuation, use
          -- putStrLn' instead of putStrLn

The solution is pretty straightforward - the messages go into a queue, and there’s a separate thread that actually does the printing. I’ve gone through a couple of iterations over the years, though, regarding how to manage the lifetime of the threads. You want to make sure they both get killed when there’s an exception to avoid leaving hanging threads, but you also want the queue worker to be able to outlast the main thread because if you kill it too eagerly at a program’s conclusion then you end up dropping any log messages that are still enqueued. The approach I’ve ended up with is to use the async package’s concurrently function for the forking, and a TVar for the main thread to send a ‘stop’ signal to the worker thread in the event of non-exceptional termination, at which point the worker finishes its queue and returns. Anyway, this was all just tedious and commonly-needed enough to be worth wrapping up into a convenient package. Hope it’s useful :slight_smile:

7 Likes

…and for ol’ beasties like me who survived the asteroid impact that are now wondering what’s wrong with that syntax:

import Unfork

main :: IO ()
main =
    unforkAsyncIO_ putStrLn $ \putStrLn' ->
        -- Within this continuation, use
        -- putStrLn' instead of putStrLn

        concurrently_ (putStrLn' "alpha") (putStrLn' "beta")

…have fun!

1 Like

You might be solving a symptom, i.e. that lazy IO is a mistake and use of putStrLn is ill advised beyond beginner book chapters. It locks MVar in the handle to update the buffer for each individual character, which is a terrible warped idea that only serves “cool” lazy I/O demos.

Use putStrLn from bytestring’s strict modules and then your writes to the Handle are atomic, and way faster. This is generally my advice to Haskell teams when consulting.

But the package itself is a fun idea independently.

Which of the following is “different”?

  • freeSTRef :: STRef s a -> ST s ()
  • closeIORef :: IORef a -> IO ()
  • endMVar :: MVar a -> IO ()
  • discardTVar :: TVar -> STM ()
  • hClose :: Handle -> IO ()
  • finalizeForeignPtr :: ForeignPtr a -> IO ()

…out of all these dismissive definitions, the last two - hClose and finalizeForeignPtr - actually do exist. As for the rest, what service they could provide in the language is much more reliably performed by the implementation!

It’s long past time that annoyances like hClose and finalizeForeignPtr be disposed of, to be replaced by implementation mechanisms similar to GC but adapted for use with Handles and ForeignPtrs.

Interleaved output is not related to lazy IO in my opinion. It can occur just the same in languages that don’t even have nonstrictness (like C!).

1 Like

If I understand them correctly, the hPutStr functions for lazy bytestring and text don’t involve “lazy I/O”, right? They’re simply not atomic.

In my real-world applications I’m not using Prelude’s putStrLn, I just wrote the docs that way to keep things simple. Maybe I shouldn’t have.

2 Likes