Haskell forkIO threads writing on top of each other with putStrLn

Simply put, putStrLn is not an atomic operation. Every character may be interleaved with any other from a different thread.

(I am also not sure about whether in multi-byte encodings such as UTF8 it is guaranteed that a multi-byte character is atomically handled.)

If you want atomicity, you can use a shared mutex e.g.

do lock <- newMVar ()
   let atomicPutStrLn str = takeMVar lock >> putStrLn str >> putMVar lock ()
   forkIO $ forever (atomicPutStrLn "hello")
   forkIO $ forever (atomicPutStrLn "world")

As suggested in the comments below, we can also simplify and make the above exception-safe as follows:

do lock <- newMVar ()
   let atomicPutStrLn str = withMVar lock (\_ -> putStrLn str)
   forkIO $ forever (atomicPutStrLn "hello")
   forkIO $ forever (atomicPutStrLn "world")