Named Lock Collection in C#?

2019-05-07 18:07发布

问题:

I have multiple threads writing data to a common source, and I would like two threads to block each other if and only if they are touching the same piece of data.

It would be nice to have a way to lock specifically on an arbitrary key:

string id = GetNextId();
AquireLock(id);
try
{
    DoDangerousThing();
}
finally
{
    ReleaseLock(id);
}

If nobody else is trying to lock the same key, I would expect they would be able to run concurrently.

I could achieve this with a simple dictionary of mutexes, but I would need to worry about evicting old, unused locks and that could become a problem if the set grows too large.

Is there an existing implementation of this type of locking pattern.

回答1:

You can try using a ConcurrentDictionary<string, object> to create named object instances. When you need a new lock instance (that you haven't used before), you can add it to the dictionary (adding is an atomic operation through GetOrAdd) and then all threads can share the same named object once you pull it from the dictionary, based on your data.

For example:

// Create a global lock map for your lock instances.
public static ConcurrentDictionary<string, object> GlobalLockMap =
    new ConcurrentDictionary<string, object> ();

// ...

var oLockInstance = GlobalLockMap.GetOrAdd ( "lock name", x => new object () );

if (oLockInstance == null)
{
    // handle error
}

lock (oLockInstance)
{
    // do work
}


回答2:

The lock keyword (MSDN) already does this.

When you lock, you pass the object to lock on:

lock (myLockObject)
{
}

This uses the Monitor class with the specific object to synchronize any threads using lock on the same object.

Since string literals are "interned" – that is, they are cached for reuse so that every literal with the same value is in fact the same object – you can also do this for strings:

lock ("TestString")
{
}

Since you aren't dealing with string literals you could intern the strings you read as described in: C#: Strings with same contents.

It would even work if the reference used was copied (directly or indirectly) from an interned string (literal or explicitly interned). But I wouldn't recommend it. This is very fragile and can lead to hard-to-debug problems, due to the ease with which new instances of a string having the same value as an interned string can be created.

A lock will only block if something else has entered the locked section on the same object. Thus, no need to keep a dictionary around, just the applicable lock objects.

Realistically though, you'll need to maintain a ConcurrentDictionary or similar to allow your objects to access the appropriate lock object.



回答3:

You can use the ConcurrentDictionary<string, object> to create and reuse different locks. If you want to remove locks from the dictionary, and also to reopen in future the same named resource, you have always to check inside the critical region if the previously acquired lock has been removed or changed by other threads. And take care to remove the lock from the dictionary as the last step before leaving the critical region.

    static ConcurrentDictionary<string, object> _lockDict =
        new ConcurrentDictionary<string, object>();

    // VERSION 1: single-shot method

    public void UseAndCloseSpecificResource(string resourceId)
    {
        bool isSameLock;
        object lockObj, lockObjCheck;
        do
        {
            lock (lockObj = _lockDict.GetOrAdd(resourceId, new object()))
            {
                if (isSameLock = (_lockDict.TryGetValue(resourceId, out lockObjCheck) && 
                                  object.ReferenceEquals(lockObj, lockObjCheck)))
                {
                    // ... open, use, and close resource identified by resourceId ...

                    // This must be the LAST statement
                    _lockDict.TryRemove(resourceId, out lockObjCheck);
                }
            }
        }
        while (!isSameLock);
    }

    // VERSION 2: separated "use" and "close" methods
    //            (can coexist with version 1)

    public void UseSpecificResource(string resourceId)
    {
        bool isSameLock;
        object lockObj, lockObjCheck;
        do
        {
            lock (lockObj = _lockDict.GetOrAdd(resourceId, new object()))
            {
                if (isSameLock = (_lockDict.TryGetValue(resourceId, out lockObjCheck) && 
                                  object.ReferenceEquals(lockObj, lockObjCheck)))
                {
                    // ... open and use (or reuse) resource identified by resourceId ...
                }
            }
        }
        while (!isSameLock);
    }

    public bool TryCloseSpecificResource(string resourceId)
    {
        bool result = false;
        object lockObj, lockObjCheck;
        if (_lockDict.TryGetValue(resourceId, out lockObj))
        {
            lock (lockObj)
            {
                if (_lockDict.TryGetValue(resourceId, out lockObjCheck) && 
                    object.ReferenceEquals(lockObj, lockObjCheck))
                {
                    result = true;
                    // ... close resource identified by resourceId ...

                    // This must be the LAST statement
                    _lockDict.TryRemove(resourceId, out lockObjCheck);
                }
            }
        }
        return result;
    }