MicroHs icon indicating copy to clipboard operation
MicroHs copied to clipboard

Deadlock detection too eager in the presence of `newStablePtr` and `apply_sp`

Open HeinrichApfelmus opened this issue 7 months ago • 9 comments

Ouch. I think I found an issue with StablePtr and concurrency.

While working on threepenny-gui again, I find myself in a situation where I need to run an event loop in a thread (that is separate from the main thread) that reads events from a TQueue. Every now and then, the JavaScript side calls apply_sp to write an event to the queue. Thanks to concurrency, the event loop will pick it up at the appropriate time — typically only after the call to apply_sp has finished.

The issue is this: The single occurrence of writeTQueue events is exported to the JavaScript side via newStablePtr. The event loop uses readTQueue events, but the Haskell side thinks that this is a deadlock, because it cannot see that the JavaScript can, in principle, still write to the events :: TQueue.


Long story short: I expect that the following program

import Control.Monad
    ( join )
import Control.Concurrent.MVar
    ( MVar, newEmptyMVar, putMVar, takeMVar )
import Control.Concurrent
    ( forkFinally )
import Foreign.StablePtr
    ( StablePtr, deRefStablePtr, newStablePtr, castStablePtrToPtr )

main :: IO ()
main = do
    putStrLn "Start of main thread"
    done   <- newEmptyMVar
    events <- newEmptyMVar
    ptr    <- newStablePtr $ putMVar events ()
    print $ castStablePtrToPtr ptr
    forkFinally (eventLoop events) (\_ -> putMVar done ())  
    -- putMVar events ()
    takeMVar done

eventLoop :: MVar () -> IO ()
eventLoop mvar = do
    _ <- takeMVar mvar
    putStrLn "End of eventLoop"

prints

Start of main thread
0x1

to the console and hangs. However, the actual behavior is that the program prints

Start of main thread
0x1
ERR: deadlock

and exits.

Uncommenting the line -- putMVar events () makes the program finish. I think this means that events is garbage collected, even though it should be protected by the call to newStablePtr. (EDIT: I'm no longer sure about this claim.)

The print $ castStablePtrToPtr ptr statement is just for show, to indicate that a foreign runtime could use the printed value with apply_sp to put something into the events variable.

(EDIT: Sorry, in the previous version, I think I got the expected result wrong. It's expected for the program to hang instead of detecting a deadlock. GHC does the expected thing.)

HeinrichApfelmus avatar Sep 30 '25 22:09 HeinrichApfelmus

I don't think there are any guarantees if you use apply_sp in a threaded setting. I asked about this, and you said there was going to be no multi threading. At least that's how I interpreted the answer.

I will have to think about how to make that work.

augustss avatar Sep 30 '25 23:09 augustss

I agree that there should be no deadlock here. I'll fix that. I need to scan the StablePtr table for MVars.

augustss avatar Sep 30 '25 23:09 augustss

I think apply_sp could be written in Haskell for GHC and exported with foreign export.

augustss avatar Sep 30 '25 23:09 augustss

To flesh out my comments above. It seems that you have one thread executing the MicroHs runtime and another thread in JS that sometimes calls apply_sp. This is definitely not allowed. The only way apply_sp is guaranteed to work is

  • a single thread executes MicroHs runtime
  • MicroHs Haskell code calls JS
  • JS calls back using apply_sp

If there are multiple threads things can go very wrong, because there is no lock protecting the RTS data structures. It assumes a single thread.

If you need multiple threads I can probably make a hack for that and have a pthread mutex protecting the RTS data structures. I think I can make the overhead really small by having the RTS hold the mutex almost all the time.

augustss avatar Oct 01 '25 12:10 augustss

I asked about this, and you said there was going to be no multi threading. At least that's how I interpreted the answer.

Sorry about my confusion. 🙈 What I meant was that I don't need concurrency in the sense that at any point in time, I only need one Haskell thread to something useful while all other Haskell threads are waiting on some blocked MVars. In this way, the program is not sequential in the strict sense, but it does have a well-defined sequence. I think that I need multi-threading in order to have more control over that sequence.

Essentially, I want to use Haskell threads to decouple

  • JS calls Haskell code using apply_sp → MicroHs Haskell code calls JS code

into

  • JS calls Haskell code using apply_sp → write call arguments in a TQueue
  • MicroHs code reads TQueue → MicroHs Haskell code calls JS code

At least, this is what the web server version of threepenny-gui does, and I'm seeing some differences in semantics for the CurrencyConverter example which — I think — are due to the different implementation that uses the MicroHs JS FFI.

The only way apply_sp is guaranteed to work is

  • a single thread executes MicroHs runtime
  • MicroHs Haskell code calls JS
  • JS calls back using apply_sp

Ah. 🤔 I was hoping that I could use Haskell threads to pause JS calls from apply_sp and pick them up later. But it seems that the main Haskell threads needs to run to completion before apply_sp can even be called? 🤔

On the other hand, maybe I can use JS code to do the shepherding of calls to apply_sp? 🤔

Also, I think I need to write down more clearly what my desired sequence of calls is. 😅 Essentially, I suspend execution in order to break nested JS → Haskell → JS → Haskell → … calls.

HeinrichApfelmus avatar Oct 01 '25 13:10 HeinrichApfelmus

I still don't understand your situation. Are there multiple (non-Haskell) threads?

Something I didn't spell out is that the code executed by apply_sp must not suspend. If it does, then crazy things will happen.

I think that you might want more than what is currently supported, where FFI calls can suspend and other Haskell threads can continue running.

augustss avatar Oct 01 '25 14:10 augustss

I still don't understand your situation. Are there multiple (non-Haskell) threads?

Let me try to illustrate my situation with a picture.

The following picture contains four distinct diagrams labeled 1) to 4).

Each diagram contains two vertical gray lines labeled "JS" and "Haskell". These represent the JS and Haskell runtimes, respectively. These runtimes run in separate threads (processes) for the "web server" variant of threepenny-gui, but for the "MicroHs" variant of threepenny-gui, the JS runtime and the Haskell runtime currently run in a single thread.

Image

In each diagram,

  • a vertical solid black line indicates the passage of time due to some computation
  • a solid horizontal arrow indicates a procedure call with a transfer of control to the other runtime
  • a solid horizontal line indicates giving back control to the caller
  • a dashed horizontal line indicates a concurrent procedure call, with immediate return of control to the caller (think forkIO).

Diagram 1) shows the default situation where the main Haskell program starts and calls a JS function every now and then.

Diagram 2) shows an optimization that makes sense for the "web server" variant of threepenny-gui: For some JS procedures, the Haskell side only cares about the side effect, not the return value, and we "fire and forget" their execution in the JS runtime.

Diagram 3) represents the situation where the main Haskell program has ended, but certain UI events, such as clicking a mouse button or entering characters in an input box triggers the execution of Haskell code. Here, the JS runtime calls apply_sp and Haskell code calls a couple of JS procedures to show the effects of the button click.

Note that the diagram 3) represents two levels of "nesting": One arrow is followed by a second arrow without a horizontal solid line in between that would give back control to the procedure caller.

Diagram 4) shows how the "web server" variant of threepenny-gui works around the nesting in 3): As I don't want to support nested procedure calls, I treat the initial call from JS to Haskel as a "fire and forget" call. This works for events such as button clicks as these situations do not expect a return value.

For the "MicroHs" variant of threepenny-gui, we have the following situation:

  • Diagram 1) works.
  • Diagram 2) is something that I need to implement for compatibility reasons. I think I can do this with multi-threading in Haskell-land.
  • Diagram 3) or diagram 4) are necessary in order to support events like button clicks.

In the context of this Github issue here, I was trying to implement 4) by using multi-threading that can switch between the JS runtime and the Haskell runtime, which may be misguided.

HeinrichApfelmus avatar Oct 04 '25 19:10 HeinrichApfelmus

I just realized that apply_sp has a bad bug. It can randomly fail to return to the caller. I'll fix that soon. And I'll think more about your use case. I might have to bite the bullet and use pthreads to get multiple threads for use in FFI calls.

augustss avatar Oct 10 '25 21:10 augustss

I just realized that, in principle, it is possible to exchange information between the JavaScript runtime and the Haskell runtime without foreign import or foreign export — by mimicking inter-process communication and exchanging messages via stdin and stdout. This would essentially match what the "web server" variant of threepenny-gui is currently doing (except that it's using HTTP instead of stdin/stdout), and would be a standard thing to do in a C/UNIX environment.

But unfortunately, the above idea requires preemptive multitasking — and as far as I can tell, the JavaScript runtime in browsers uses co-operative multitasking. Ouch. 🙈 I will investigate further.

HeinrichApfelmus avatar Nov 11 '25 22:11 HeinrichApfelmus