For those such as myself who stumble across this thread in the future - the answer given by @gabba works fantastically (tested with accessing the same file from 2048 concurrent threads and performing writing, reading, and file deletion), but if counting the references is not important, the following code:
public class NamedLock
{
private class LockAndRefCounter
{
public long refCount;
}
private ConcurrentDictionary<string, LockAndRefCounter> locksDictionary = new ConcurrentDictionary<string, LockAndRefCounter>();
public void DoWithLockBy(string key, Action actionWithLock)
{
var lockObject = new LockAndRefCounter();
var keyLock = locksDictionary.GetOrAdd(key, lockObject);
Interlocked.Increment(ref keyLock.refCount);
lock (keyLock)
{
actionWithLock();
Interlocked.Decrement(ref keyLock.refCount);
if (Interlocked.Read(ref keyLock.refCount) <= 0)
{
LockAndRefCounter removed;
locksDictionary.TryRemove(key, out removed);
}
}
}
}
can be reduced to:
public class NamedLock
{
private class LockObject { /* Empty */ }
private static readonly ConcurrentDictionary<string, LockObject> locks = new();
public void DoWithLockBy(string key, Action actionWithLock)
{
LockObject _lock = locks.GetOrAdd(key, new LockObject());
lock (_lock)
{
action();
}
}
}
The result is an empty object for each file path - i.e. a "named" lock. Instead of removing the lock object from the dictionary, it is used by the lock
statement as a mutex for all threads that access this file path.