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