You should build an type around the file handle and a mutex lock. Here's a simple implementation that I think would work for your purposes.
module SharedHandle (SharedHandle, newSharedHandle, withSharedHandle) where
import Control.Concurrent.MVar
import System.IO
data SharedHandle = SharedHandle Handle (MVar ())
newSharedHandle :: IO Handle -> IO SharedHandle
newSharedHandle makeHandle = do
handle <- makeHandle
lock <- newMVar()
return $ SharedHandle handle lock
withSharedHandle :: SharedHandle -> (Handle -> IO a) -> IO a
withSharedHandle (SharedHandle handle lock) operation = do
() <- takeMVar lock
val <- operation handle
putMVar lock ()
return val
What's doing on here is I've created a new datatype which is, at it's essence, just a file handle. The only difference is that it also comes with its own individual mutex lock implemented with an MVar. I have provided two functions for operating on this new type. newSharedHandle takes a operation that would create a normal handle and created a shared handle with a fresh lock. withSharedHandle takes an operation for operating on handles, locks the shared handle, performs the operation, and then unlocks the handle. Notice that the constructor or accessors are not provided from the module so we can be assured no process ever forgets to free the lock and we never get deadlocks on one particular access.
Replacing all file handles in your program with this new type could solve your problem.