Skip to content

Instantly share code, notes, and snippets.

@frasertweedale
Created December 5, 2019 06:46
Show Gist options
  • Save frasertweedale/f6e6b7fc782ed70affa5c0328e73fe6b to your computer and use it in GitHub Desktop.
Save frasertweedale/f6e6b7fc782ed70affa5c0328e73fe6b to your computer and use it in GitHub Desktop.
Horrible haskell FFI concurrency talloc bug

Notes about a very hard to debug bug:

Eventually I deduced that it was lack of thread safety in talloc causing corruption and (most of the time) an abort() when talloc itself detected the corruption.

So, I created pthread mutexs around all operations that might do talloc things. Including routines called from finalisers.

This resulted in instant deadlocks. Why? forkIO was being used, instead of forkOS to create bound threads. Because Haskell execution capability can move among OS threads, in some cases the lock was acquired by one OS thread, released by a different thread (which does not actually unlock it), and then the next attempt to acquire the lock deadlocks.

OK, so we have to use bound threads for everything. No worries, switch to forkOS.

Still, instant deadlocks. So, what now? A footnote in the Control.Concurrent module documentation contains a clue:

There is a subtle interaction between deadlock detection and finalizers (as created by newForeignPtr or the functions in System.Mem.Weak): if a thread is blocked waiting for a finalizer to run, then the thread will be considered deadlocked and sent an exception. So preferably don't do this, but if you have no alternative then it is possible to prevent the thread from being considered deadlocked by making a StablePtr pointing to it. Don't forget to release the StablePtr later with freeStablePtr.

OK, so a strong dose of StablePtr should fix it. The following forkOS wrapper automatically creates and destroys a StablePtr to the new thread spawned to execute the action:

-- | Variant of forkOS that creates a StablePtr to the new thread.
-- This can be used to prevent garbage collection of a thread that
-- is blocked waiting for a finalizer to run.
--
forkOSStableThread :: IO () -> IO ThreadId
forkOSStableThread action = do
  mv <- newEmptyMVar  -- new MVar to hold the StablePtr
  let
    newAction =
      bracket
        (takeMVar mv)   -- wait to receive the StablePtr
        freeStablePtr   -- free the StablePtr
        (const action)  -- execute the original action
  tid <- forkOS newAction
  sp <- newStablePtr tid
  putMVar mv sp
  pure tid

And with that (and a small patch to brick where the main event loop thread gets spawned) the deadlocks are gone.

But the crash behaviour persists :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment