Ilya Kozhevnikov
Ilya Kozhevnikov

Reputation: 10432

Mutex alternative for process synchronisation/signalling with async support?

Is there anything except for Mutex to synchronise two processes in a fault-tolerant fashion? Please bear with me...

There is a process A, it's a bit flaky, it needs to start process B in the background and continue. If process A successfully does its thing, it needs to signal process B to dispose, and moves on (it doesn't terminate and thread is reused). If process A dies due to exception, termination, etc. process B needs to detect it quickly and dispose of itself on its own. Process A is not a "process" rather a library executed by various hosts hence process B can't just wait for process A's name to disappear.

Enter Mutex.

Here process A represented by a test fixture, if successful it'll call TestFixtureTearDown and move on, or test runner might be killed and TestFixtureTearDown is never executed. As with the actual process, TestFixtureTearDown might be called by a different thread to one that ran TestFixtureSetUp and created the mutex, hence ReleaseMutex sometimes throws ApplicationException : Object synchronization method was called from an unsynchronized block of code.

  1. Can I force ReleaseMutex in TestFixtureTearDown if it's being executed by a different thread or abandon mutex some other way?

  2. Is there an alternative to Mutex that I can use for such fault-tolerant "reverse" wait/monitor scenario? Preferably without implementing process A sending heartbeats to process B and process B tracking intervals and timing out? Mutex felt like such an elegant solution except for occasional ApplicationException on asyncs.

.

namespace ClassLibrary1
{
    public class Class1
    {
        private Mutex _mutex;
        private Process _process;

        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
            _mutex = new Mutex(true, "foo");
            _process = Process.Start("ConsoleApplication1.exe");
        }

        [Test]
        public void Test1() { /* Do stuff */ }

        [Test]
        public void Test2() { /* Do async stuff */ }

        [TestFixtureTearDown]
        public void TestFixtureTearDown()
        {
            _mutex.ReleaseMutex();
            _process.WaitForExit();
        }
    }
}

.

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var mutex = Mutex.OpenExisting("foo");

            // Start doing stuff

            try { mutex.WaitOne(); }
            catch (AbandonedMutexException) { }
            finally { mutex.ReleaseMutex(); }

            // Finish doing stuff
        }
    }
}

Upvotes: 2

Views: 1731

Answers (2)

Ilya Kozhevnikov
Ilya Kozhevnikov

Reputation: 10432

I ended up using a mix of Mutex, Thread and ManualResetEvent. For the googling folk of the future here's a verbose test:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace MutexResetEvent.Tests
{
    public class Class1
    {
        private Mutex _mutex;
        private Thread _thread;
        private Process _process;
        private ManualResetEvent _event;

        [SetUp]
        public void SetUp()
        {
            Console.WriteLine("SetUp: #{0}", Thread.CurrentThread.ManagedThreadId);

            _event = new ManualResetEvent(false);
            _thread = new Thread(() =>
            {
                Console.WriteLine("Thread: #{0}", Thread.CurrentThread.ManagedThreadId);

                _mutex = new Mutex(true, "MutexResetEvent");

                _process = new Process
                {
                    StartInfo =
                    {
                        FileName = "MutexResetEvent.Worker.exe",
                        //UseShellExecute = false,
                        //RedirectStandardOutput = true
                    }
                };
                //_process.OutputDataReceived += (o, a) => Console.WriteLine(a.Data);
                _process.Start();
                //_process.BeginOutputReadLine();

                while (!_event.WaitOne(1000))
                    Console.WriteLine("Thread: ...");

                Console.WriteLine("Thread: #{0}", Thread.CurrentThread.ManagedThreadId);

                _mutex.ReleaseMutex();
                _process.WaitForExit();
            });
        }

        [Test]
        public void Test()
        {
            Console.WriteLine("Test: #{0}", Thread.CurrentThread.ManagedThreadId);

            _thread.Start();

            for (var i = 0; i < 3; i++)
            {
                Console.WriteLine("Test: ...");
                Thread.Sleep(1000);
            }

            /*
            if (Guid.NewGuid().GetHashCode() % 3 == 0)
                Environment.Exit(1);
            //*/
        }

        [TearDown]
        public void TearDown()
        {
            Console.WriteLine("TearDown: #{0}", Thread.CurrentThread.ManagedThreadId);

            Task.Run(() =>
            {
                Console.WriteLine("Task: #{0}", Thread.CurrentThread.ManagedThreadId);
                _event.Set();
                //_thread.Join();
            }).Wait();

            for (var i = 0; i < 3; i++)
            {
                Console.WriteLine("TearDown: ...");
                Thread.Sleep(1000);
            }
        }
    }
}

.

using System;
using System.Threading;

namespace MutexResetEvent.Worker
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Worker: #{0}", Thread.CurrentThread.ManagedThreadId);

            var mutex = Mutex.OpenExisting("MutexResetEvent");

            try
            {
                while (!mutex.WaitOne(1000))
                    Console.WriteLine("Worker: ...");
            }
            catch (AbandonedMutexException)
            {
                Console.WriteLine("Worker: AbandonedMutexException");
            }

            Console.WriteLine("Worker: #{0}", Thread.CurrentThread.ManagedThreadId);

            mutex.ReleaseMutex();
            Console.WriteLine("Worker: WOO HOO");

            Console.ReadLine();
        }
    }
}

Upvotes: 1

usr
usr

Reputation: 171206

Semaphores do not have thread affinity. You can release a semaphore on a different thread than it was acquired on. Use a semaphore with a count of 1.

Upvotes: 1

Related Questions