In this tip, I share a small class that synchronizes multithread access to multiple objects.
Introduction
Sometimes, we need to provide lock/release access to multiple objects by string
- i.e., list of file names where we don't want to put a lock on the entire list, but need to prevent simultaneous access to each record by name (such as file name or database object).
Background
If there is a list of the objects, it is possible (but probably not considered as a good practice) to put a lock on the object itself when working on it. In other situations (i.e., there is no list of object or the list is changing), it can be a bit tricky.
To help with this issue, I wrote a small class that can do the work - please see the code below:
The Class Code
public class MultiLock
{
private class SyncObject
{
public int Count;
public AutoResetEvent Event;
public SyncObject()
{
Event = new AutoResetEvent(true);
}
}
private Dictionary<string, SyncObject> syncs;
public MultiLock()
{
syncs = new Dictionary<string, SyncObject>();
}
public void Lock(string Name)
{
SyncObject so;
lock (syncs)
{
if (!syncs.TryGetValue (Name, out so))
{
so = new SyncObject();
syncs[Name] = so;
}
Interlocked.Increment(ref so.Count);
}
so.Event.WaitOne();
}
public void Release(string Name)
{
SyncObject so;
lock (syncs)
{
if (!syncs.TryGetValue(Name, out so))
return;
if (Interlocked.Decrement(ref so.Count) == 0)
syncs.Remove(Name);
so.Event.Set();
}
}
}
This class contains the Dictionary
, that maps object name (i.e., file name) to the synchronization object. When one calls the Lock
method, checks whether this name is already in lock, and if it isn't, adds it to the dictionary, otherwise, it will increase the locks Counter
. This action is wrapped into the lock, put on entire dictionary, but it is expected to be very short, just to avoid crash on the collection change from the other thread.
When the Release
method is called, it checks whether it is last lock removed (Counter == 0
) and if yes, it also removes this name from the list.
Code Usage
Here is the sample of the code usage:
class Program
{
static MultiLock ml;
static Dictionary<string, List<int>> keys =
new Dictionary<string, List<int>>();
static void Main(string[] args)
{
for (int i = 0; i < 11; i++)
{
keys["X" + i] = new List<int>();
}
Stopwatch sw = new Stopwatch();
sw.Start();
ml = new MultiLock();
Task[] tasks = new Task[50];
for (int i = 0; i < tasks.Length; i++)
{
int n = i;
string key = keys.Keys.Skip(i % keys.Count).First();
tasks[i] = ThreadFunc(n, key);
}
Task.WaitAll(tasks);
sw.Stop();
Console.WriteLine("Run ended in " +
sw.Elapsed.TotalSeconds.ToString("0.##") + " sec, press <Enter> to exit");
Console.ReadLine();
}
static async Task ThreadFunc(int n, string key)
{
Random rnd = new Random();
var list = keys[key];
for (int k = 0; k < 100; k++)
{
ml.Lock(key);
if (list.Count > 0)
Console.WriteLine("Error 1");
list.Add(n);
await Task.Delay(rnd.Next(0, 20));
list.Remove(n);
if (list.Count > 0)
Console.WriteLine("Error 2");
ml.Release(key);
}
}
}
Update
After testing, I discovered that the use of 'async
' methods caused the Monitor.Exit
method to throw a SynchronizationLockException
. This was due to the requirement that the Monitor.Exit
method be called from the same thread as its Monitor.Enter
. To resolve this issue, I've replaced the Monitor
with the AutoresetEvent
, which does not have this limitation.
There are also minor changes in the code usage - mostly to shorten the code
History
- 26th November, 2022: Initial version
- 29th January, 2023: Update