Opened 3 years ago

Last modified 3 months ago

#10793 new bug

Incorrect blocked on MVar detection

Reported by: NeilMitchell Owned by:
Priority: normal Milestone:
Component: Runtime System Version: 7.10.1
Keywords: Cc: ndmitchell@…, simonmar, osa1
Operating System: Windows Architecture: Unknown/Multiple
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: #10241 Differential Rev(s):
Wiki Page:

Description

Given the code:

import Control.Concurrent

main = do
    v <- newEmptyMVar
    forkFinally (putStrLn $ let x = x in x) (putMVar v)
    print =<< takeMVar v

The spawned thread should raise a NonTermination exception (assuming idle GC), then put it in the MVar v, which should cause the main thread to abort printing Left <<loop>>.

However, in reality, the main thread aborts with Exception: thread blocked indefinitely in an MVar operation.

My debugging shows that the spawned thread raises NonTermination and at the same time the main thread raises BlockedIndefinitelyOnMVar. Of course, the main thread is not really blocked on the exception, and after the finally bit runs, the MVar is filled. Adding tracing shows that the putMVar is called.

Hypothesis is that the GC notices it can raise the non-termination exception, and then opportunistically excludes that thread from further GC scanning, and detects the MVar is unreferenced, raising the subsequent exception. Of course, given you can catch the exception, that isn't safe. Roman Leshchinskiy worries that in the wrong circumstances the MVar might really be being GC'd and thus could corrupt memory. This issue was detected while debugging an error in the Shake build system for GHC itself.

Change History (6)

comment:1 Changed 3 years ago by simonmar

Resolution: wontfix
Status: newclosed

This is working as intended. At the time the forked thread is deadlocked, the main thread is also deadlocked (the dependency between them is not visible to the GC). If you want to keep the main thread alive manually, you can do this:

  newStablePtr =<< myThreadId

from the main thread.

comment:2 Changed 3 years ago by NeilMitchell

I still don't understand, most likely because my understanding of what causes a BlockedIndefinitelyOnMVar exception is probably wrong. Is there a good link to the current semantics? And what actually triggers such an exception? Ditto for NonTermination. (And separately, should such pointers be added to the Exception docs?)

comment:3 Changed 3 years ago by simonmar

Well, thinking about it a bit more, in this case it would be possible to do something differently. We have

  • a main thread, blocked on an MVar
  • a child thread, in an infinite loop
  • the MVar, reachable from both threads.

All of these things are unreachable from the roots, so we definitely can't make progress without throwing an exception. Normally we throw exceptions to all the deadlocked threads in this case, because it's the only deterministic thing we can do. (picking a thread randomly to receive the exception doesn't seem good).

We could refine it slightly so that if there are both NonTermination exceptions and BlockedIndefinitelyOnMVar exceptions, we only throw the NonTermination exceptions. We know that waking up the BlockedIndefinitelyOnMVar threads can't unblock the NonTermination threads, but the reverse might be true.

That wouldn't change the more common case of this, which is where both threads are blocked on MVars:

main = do
    v <- newEmptyMVar
    v2 <- newEmptyMVar
    forkFinally (takeMVar v2) (putMVar v)
    print =<< takeMVar v

here we'd still get both exceptions at the same time. So I'm not sure how I feel about doing something different for NonTermination, possibly making the situation more complicated.

comment:4 Changed 3 years ago by NeilMitchell

Cc: simonmar added

That all makes sense. From my perspective, NonTermination and BlockedIndefinitelyOnMVar are separate analysis passes (which as an implementation detail you have implemented in one go), and when viewed that way, having the first analysis raise the exceptions first seems quite reasonable.

For info, I have a workaround in my case - catch the blocked exception, sleep for a second, then retry and see if the MVar is now filled. It's ugly (particularly the 1s sleep), but it works.

comment:5 Changed 3 years ago by simonmar

Owner: simonmar deleted
Resolution: wontfix
Status: closednew

comment:6 Changed 3 months ago by osa1

Cc: osa1 added
Note: See TracTickets for help on using tickets.