3
\$\begingroup\$

I have two windows services working in parallel. Each of them is using a network drive that is connected and disconected dynamically by each one as required. They do it by executing net use command. (This is necessary because there've been issues with automatic reconnection when the server is rebooted. Not it's running stable.)

However, from time to time both services overrun and one of them fails to connect the drive because it's already connected by the other one.

To address this problem I thought I use a named Mutex to prevent both services trying to use that network location at the same time.

I didn't want to think about mutexes each time I want to synchronize that access so I created this small helper encapsulating a named Mutex.

public static class InterprocessSynchronizer
{
    public static async Task<T> TryExecuteAsync<T>(string resourceName, Func<CancellationToken, Task<T>> func, TimeSpan timeout, CancellationToken cancellationToken)
    {
        using (var mutex = new Mutex(initiallyOwned: false, name: resourceName))
        {
            if (mutex.WaitOne(timeout))
            {
                try
                {
                    return await func(cancellationToken);
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }
        return default(T);
    }
}

I tested it with two threads and it succeeded. I used Task.Factory.StartNew here for test purposes (mutexes require different threads) but I'm not going to use it later when they will be running in two different processes anyway.

[TestClass]
public class InterprocessSynchronizerTest
{
    [TestMethod]
    public async Task TryExecuteAsync_TwoTasks_Synchronized()
    {
        var task1 = Task.Factory.StartNew(async () => await InterprocessSynchronizer.TryExecuteAsync("foo", token =>
        {
           Thread.Sleep(2000);
           return Task.FromResult("abc");
        }, TimeSpan.FromMilliseconds(-1), CancellationToken.None));

        var task2 = Task.Factory.StartNew(async () => await InterprocessSynchronizer.TryExecuteAsync("foo", token =>
        {
            Thread.Sleep(3000);
            return Task.FromResult("xyz");
        }, TimeSpan.FromMilliseconds(-1), CancellationToken.None));

        var stopwatch = Stopwatch.StartNew();

        var result = await Task.WhenAll(task1, task2);

        stopwatch.Stop();

        Assert.AreEqual("abc", task1.Result.Result);
        Assert.AreEqual("xyz", task2.Result.Result);

        Assert.IsTrue(stopwatch.ElapsedMilliseconds >= 5000);
    }
}

Is this solution acceptable or am I doing something terribly wrong?

\$\endgroup\$
2
\$\begingroup\$

Unfortunately you are in fact doing something wrong, but maybe not terribly! Because you are accessing the Mutex either side of an await, there is no guarantee that the same thread will be accessing it each time. The question and answer here have more detail. This is only a problem if your SynchronizationContext doesn't guarantee async calls will return to the same thread (as I suspect is the case when running your tests). If you only call this code from the UI thread of a WPF app for example, you won't have any issues. The code breaks when used in a console app however.

public class Program
{
    public static async Task Main()
    {
        using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
        {
            await Task.WhenAll(
                InterprocessSynchronizer.TryExecuteAsync("resource", c => SwitchThreads("FIRST", c), TimeSpan.FromSeconds(60), cts.Token),
                InterprocessSynchronizer.TryExecuteAsync("resource", c => SwitchThreads("SECOND", c), TimeSpan.FromSeconds(60), cts.Token)
            );
        }
    }

    private static async Task<int> SwitchThreads(string name, CancellationToken arg)
    {
        Console.WriteLine($"{name} ON THREAD ID {Thread.CurrentThread.ManagedThreadId}");
        await Task.Delay(1000, arg);
        Console.WriteLine($"{name} NOW ON THREAD ID {Thread.CurrentThread.ManagedThreadId}");
        return 0;
    }
}

Object synchronization method was called from an unsynchronized block of code

If you encounter this error some of the suggesting in the afformentioned SO question could help.

\$\endgroup\$
  • \$\begingroup\$ You're absolutely right. Maybe I should check the thread ids manually and throw some more helpful exception about this issue. \$\endgroup\$ – t3chb0t Jul 23 '18 at 19:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.