Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Class to Synchronize Multithread Access to Multiple Objects

4.93/5 (13 votes)
29 Jan 2023CPOL2 min read 13.1K  
Synchronize access to multiple objects by name (string)
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

C#
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:

C#
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)