The mysterious life of mutexes

time to read 21 min | 4012 words

Last night I was talking with Ron Grabowski (of log4net fame) and the subject of mutexes came up. Specifically, the subject of abandoned mutexes across threads, AppDomains, and Processes. I was pretty sure what the behavior should be, but we wrote a set of test cases for that, with... surprising results.

My expectation that after I get a hold on a mutex, I have to explicitly let it go. If I am killed or interrupted somehow, then I would expect the next person to acquire the mutex to get an AbandonedMutexException.

The first code sample does exactly that:

new Thread(delegate()
{
    Mutex m = new Mutex(true, "foo");
    m.WaitOne();
    Console.WriteLine("Got mutex");
    Thread.Sleep(3000);
    Console.WriteLine("Bye..");
}).Start();
 
Thread.Sleep(500);// give the thread some time to get the mutex
Mutex m2 = new Mutex(true, "foo");
m2.WaitOne();

We haven't explicitly release the mutex, but we let the thread die, on acquire, you'll get the expected AME.

Now, let us try it with processes, shall we?

if (args.Length != 0)
{
    Mutex m = new Mutex(true, "foo");
    m.WaitOne();
    Console.WriteLine("Got mutex");
    Thread.Sleep(1000);
    Console.WriteLine("Bye, fast");
    Environment.FailFast("blah");
}
else
{
    Process p = Process.Start(typeof(Program).Assembly.Location, "a");
    Thread.Sleep(2000);
    Mutex m = new Mutex(true, "foo");
    m.WaitOne();
    Console.WriteLine("got it second");
}

We start our process, which spawn a second process, which would acquire a lock on the mutex, and then fail fast, killing the process. The first process, on attempting to get the process, will die horribly, as expected.

But not so fast, because this bit of code doesn't throw anything, and pretend that everything is right.

if (args.Length != 0)
{
    Mutex m = new Mutex(true, "foo");
    m.WaitOne();
    Console.WriteLine("Got mutex");
    Thread.Sleep(1000000);
}
else
{
    Process p = Process.Start(typeof(Program).Assembly.Location, "a");
    Thread.Sleep(2000);
    p.Kill();
    Mutex m = new Mutex(true, "foo");
    m.WaitOne();
    Console.WriteLine("got it second");
}

I am not really sure why this is the case, I would expect Envirionment.FailFast and an external Process.Kill to have the same behavior.

It gets to be even stranger, because we see the exact same problem when we are dealing with AppDomains:

public class Foo : MarshalByRefObject
{
    public void Repeat()
    {
        bool ignored;
        Mutex m = new Mutex(true, "blah", out ignored);
        m.WaitOne();
        while (1 != DateTime.Now.Ticks)
        {
            Console.Write(".");
            Thread.Sleep(1000);
        }
    }
}
 
AppDomain app = AppDomain.CreateDomain("fo");
new Thread(delegate()
{
    Foo f = (Foo)app.CreateInstanceAndUnwrap(
        typeof(Foo).Assembly.FullName,
        typeof(Foo).FullName);
    f.Repeat();
}).Start();
Thread.Sleep(1000);
bool ignored;
Mutex m3 = new Mutex(true, "blah", out ignored);
Console.Write("aquiring");
m3.WaitOne(TimeSpan.FromSeconds(2), false);
AppDomain.Unload(app);
Console.Write("done");

I am not willing to comment on what is going on, I have the feeling that I am missing something.

Nevertheless, this is surprising behavior.