0

We have a legacy VB6 executable that can run multiple instances. We'd like certain jobs only allow one concurrent instance.

It seems that an OS Mutex is a perfect fit, as this is a legacy app all new code must be written in C# and accessed over interop.

I have created a class that will acquire:

public bool AcquireLock(string JobId)
{
    // get application GUID as defined in AssemblyInfo.cs
    string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
    appGuid = appGuid + JobId;

    // unique id for global mutex - Global prefix means it is global to the machine
    string mutexId = string.Format("Global\\{{{0}}}", appGuid);

    bool mutexExists = false;

    var mutex = new Mutex(true, mutexId, out mutexExists);
    var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
    var securitySettings = new MutexSecurity();
    securitySettings.AddAccessRule(allowEveryoneRule);
    mutex.SetAccessControl(securitySettings);

    return mutexExists;
}

And release locks:

public bool ReleaseLock(string JobId)
{
    // get application GUID as defined in AssemblyInfo.cs
    string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
    appGuid = appGuid + JobId;

    // unique id for global mutex - Global prefix means it is global to the machine
    string mutexId = string.Format("Global\\{{{0}}}", appGuid);

    var mutex = Mutex.OpenExisting(mutexId);

    mutex.ReleaseMutex();

    return true;
}

All seems to work well enough until I try to release the lock:

[TestMethod()]
public void ReleaseLockTest()
{
    var target = new MutexConcurrencyHelper();
    var jobId = RandomUtils.RandomString(8, true);
    var expected = true;
    bool actual;
    actual = target.AcquireLock(jobId);
    Assert.AreEqual(expected, actual);

    target.ReleaseLock(jobId);

    var expected1 = true;
    bool actual1;
    actual1 = target.AcquireLock(jobId);
    Assert.AreEqual(expected1, actual1);
}

The second attempt at gaining a lock find the lock already in place. Why does this lock not release?

Arjun Sol
  • 731
  • 5
  • 18
  • One thing i noticed is you never called `Dispose()` on the `Mutex` in `ReleaseLock()` – EkoostikMartin Sep 26 '13 at 15:11
  • A fair point, though I just wrapped it in a using statment: `using (var mutex = Mutex.OpenExisting(mutexId))` to no avail. – Arjun Sol Sep 26 '13 at 15:16
  • Also if I chnage the Mutex instantiation to `new Mutex(false, mutexId, out mutexExists);` I get an `ApplicationException: Object synchronization method was called from an unsynchronized block of code.` – Arjun Sol Sep 26 '13 at 15:18
  • 1
    You'll definitely need to re-think this. The mutex doesn't disappear until you Dispose() the mutex variable or it gets garbage collected. Your ReleaseLock() makes it worse, it adds *another* reference to the system object. – Hans Passant Sep 26 '13 at 16:05

2 Answers2

1

The out value on the constructor isn't what you want to return to indicate whether the mutex was acquired. It only indicates whether the specified mutex name was new. Specify initiallyOwned (the first param) as false and then return mutex.WaitOne();

You'll probably want to make AcquireLock work as a 'try to acquire lock' with a timeout. Look at this SO answer for a full example.

Community
  • 1
  • 1
jltrem
  • 12,124
  • 4
  • 40
  • 50
0

Thanks to Hans I have created a simpler solution, this also gives us the ability to limit how many instances are running which may also be desirable:

EDIT: Added GenerateMutexId for completeness.

class SemaphoreConcurrencyHelper: IConcurrencyHelper
{
    private Semaphore _semaphore;

    public bool AcquireLock(string LockId, int Instances)
    {
        try
        {
            _semaphore = Semaphore.OpenExisting(GenerateMutexId(LockId)); //Acquire existing Semaphore (if exists)                   
        }
        catch (WaitHandleCannotBeOpenedException) // Create new Semaphore if not exists
        {
            _semaphore = new Semaphore(Instances, Instances, GenerateMutexId(LockId));                
        }

        return _semaphore.WaitOne(TimeSpan.FromSeconds(10), false); // Block thread until Semaphore has slot available within specified Timespan
    }

    public bool ReleaseLock()
    {
        try
        {
            _semaphore.Release(1); // Increment the count on the Sempahore by 1
        }
        catch (Exception e)
        {
            return false; //TODO: Add an error log
        }
        _semaphore = null;
        return true;
    }

    private string GenerateMutexId(string LockId)
    {
        // Get application GUID as defined in AssemblyInfo.cs
        string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
        appGuid = appGuid + LockId;

        // Unique id for global mutex - Global prefix means it is available system wide
        return string.Format("Global\\{{{0}}}", appGuid);
    }
}

All my test cases pass so far, comments welcome.

Arjun Sol
  • 731
  • 5
  • 18