Named Lock Collection in C#?
Asked Answered
M

3

6

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.

Malign answered 3/11, 2014 at 22:28 Comment(2)
msdn.microsoft.com/en-us/library/c5kehkcz.aspxSudduth
Related: Asynchronous locking based on a keyBurrow
G
9

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
}
Generable answered 3/11, 2014 at 22:34 Comment(9)
This is basically the answer that I was going to write, but I don't think it is necessary to verify that oLockInstance isn't null. GetOrAdd should never return null since you're always returning a valid object from the lambda that you've provided.Acre
@Acre I agree with you that it shouldn't - I only added that bit as a last line of defense. I've seen strange things through the years. :)Generable
Nice and simple. Can you think of a good way to make sure the size of GlobalMapLock stays under control? I could have a lot of keys.Malign
@Malign It's probably possible but I think it'd be very hard since a lock may be used by one or more threads at a time - you can remove it from the dictionary (the threads hold onto the object instance) but if a new thread comes in that would need the same instance, it'll now create a new one and you'll have multiple concurrent threads locking on independent lock instances.Generable
@Malign How many is "lot of keys"? Having many, many objects doesn't necessarily eat up a lot of memory - it may be easier to deal with memory.Generable
Yeah, I realize I have a nice place in my outer loop where I can clear the dictionary periodically. Not really a problem anymore.Malign
@Malign If you have that, that's far, far easier than managing the lock instances while threads are using them.Generable
I suggest to use SemaphoreSlim (more async friendly) instead of lock. And please see my comment for @Edoardo B answerUltrasonic
@Ultrasonic yes, lock doesn't work in async functions, only SemaphoreSlim does.Generable
K
5

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)))
            {
                try
                {
                    // ... open, use, and close resource identified by resourceId ...
                    // ...
                }
                finally
                {
                    // 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 (result = (_lockDict.TryGetValue(resourceId, out lockObjCheck) &&
                            object.ReferenceEquals(lockObj, lockObjCheck)))
            {
                try
                {
                    // ... close resource identified by resourceId ...
                    // ...
                }
                finally
                {
                    // This must be the LAST statement
                    _lockDict.TryRemove(resourceId, out lockObjCheck);
                }
            }
        }
    }
    return result;
}
Kerr answered 28/1, 2016 at 17:6 Comment(2)
This is overcomplicated. The only problem of ConcurrentDictionary is that it is does not guarantee that value factory passed to GetOrAdd will be called only once. Using lazy as dictionary item fixes the problem (some lightweight lazy instances will be discarded without value generation).Ultrasonic
This is the only implementation of locking on a string key that appears to safely clean up the lock objects and strings in the ConcurrentDictionary.Omeara
T
4

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.

Timework answered 3/11, 2014 at 22:31 Comment(6)
Maybe my question was misleading. The lock keys are not string literals, but are from the data itself.Malign
@Malign Then you would need to create objects representing that data and keep track of them. Given the implementation of string, you might get away with just locking on the string itself, but I wouldn't count on it.Timework
@Malign Check that, it definitely won't work unless you intern the strings. See the post I linked.Timework
@BradleyDotNET: locking on strings only works if you use an interned string (typically just literals), or a reference assigned (directly or indirectly) from an interned string. The Monitor is on the specific object, so if you copy a literal to some other string instance, locking the original in one thread and the copy in another won't synchronize the threads. Because of this, I'd strongly discourage anyone from using a string literal for a lock statement. It's too easy for it to break or to be misused. Sticking with a dedicated object as in your first proposal is best IMHO.Eliathas
@PeterDuniho I tried to say that, but perhaps it wasn't clear. I'll try to edit, and if you have a better way to emphasize that, I'd be happy to take an edit from you.Timework
@BradleyDotNET: okay, I made a stab at it. I'm not entirely sure this improves it for everyone else, but it at least seems clearer/more emphatic to me. :)Eliathas

© 2022 - 2024 — McMap. All rights reserved.