My challenge:
- avoid race conditions in a method
DoWorkWithId
that's being triggered from the UI by multiple users for multiple ids, e.g.lock
- re-entrance into
DoWorkWithId
permitted for different ids but not for the same id , e.g. using aConcurrentDictionary
where id is the key and value is an object - should be a non-blocking lock so threads skip critical section without waiting and letting others know that it's already running (at least was running when invoked a second time), e.g.
Monitor.TryEnter
orInterlocked.*
My attempt:
I guess 1. and 2. could be solved using a ConcurrentDictionary
and lock
private static readonly ConcurrentDictionary<Guid, object> concurrentDictionaryMethodLock = new();
public string CallToDoWorkWithId(Guid id) // [Edited from DoWorkWithId]
{
concurrentDictionaryMethodLock.TryAdd(id, new object()); // atomic adding
lock (concurrentDictionaryMethodLock[id])
{
DoWorkWithId(id);
}
return "done";
}
Now, I don't want threads with the same id to wait. When the first thread is done, threads that waited would find out in the first line of DoWorkWithId
that there's nothing to do as the object id refers to has been already modified.
I was hoping to tackle 3. with Monitor.TryEnter
or Interlocked.*
3.1. MSDN says lock's basically
object __lockObj = x;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// Your code...
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
3.2. MSDN example of Monitor.TryEnter
var lockObj = new Object();
bool lockTaken = false;
try
{
Monitor.TryEnter(lockObj, ref lockTaken);
if (lockTaken)
{
// The critical section.
}
else
{
// The lock was not acquired.
}
}
finally
{
// Ensure that the lock is released.
if (lockTaken)
{
Monitor.Exit(lockObj);
}
}
In order to correctly release the lock I thought I need to keep track who's acquired the lock, e.g.
private static readonly ConcurrentDictionary<Guid, bool> bools = new();
.
So, I tried:
private static readonly ConcurrentDictionary<Guid, object> concurrentDictionaryMethodLock = new();
private static readonly ConcurrentDictionary<Guid, bool> bools = new();
public string CallToDoWorkWithId(Guid id) // [Edited from DoWorkWithId]
{
concurrentDictionaryMethodLock.TryAdd(id, new object());
bools.TryAdd(id, false);
try
{
Monitor.TryEnter(concurrentDictionaryMethodLock[id], ref bools[id]); // error
if (bools[id])
{
DoWorkWithId(id);
}
else
{
return "Process already running. Wait, refresh page and try again.";
}
}
finally
{
if (bools[id])
{
Monitor.Exit(concurrentDictionaryMethodLock[id]);
}
}
return "done";
}
This gives me CE CS0206: A property or indexer may not be passed as an out or ref parameter.
Same thing when using Interlocked.*
. I need to keep track of the id based booleans, here called usingResource, MSDN example.
My question:
How can I use the verbose syntax of lock and store the bools used to identify who has the lock/ressource when using a lock on a ConcurrentDictionary?
PS:
I'm also interested in better suited solutions for what I'm trying to do. I was thinking of using a ConcurrentDictionary without a lock:
private static readonly ConcurrentDictionary<Guid, string> concurrentDictionaryMethodLock = new();
public string CallToDoWorkWithId(Guid id) // [Edited from DoWorkWithId]
{
var userName = GetUserName();
if (!concurrentDictionaryMethodLock.TryAdd(id, userName)) // false means unable to add, was already added, please skip and let UI know
{
if (concurrentDictionaryMethodLock.TryGetValue(id, out var value))
{
return $"Please wait. {value} started process already.";
}
else // in case id has been removed in the meantime, even TryGetOrAdd is not atomic (MSDN remarks about valueFactory)
{
return "Process was running and finished by now. Please refresh and try again.";
}
}
try
{
DoWorkWithId(id);
}
finally
{
concurrentDictionaryMethodLock.TryRemove(id, out _);
}
return "done";
}
Any pitfalls using this approach?