Efficiently synchronize threads’ access to shared data


Accessing shared members, or static data, in an ASP.NET context must be done carefully. Actually, we must ensure that the code is thread-safe by using a synchronization mechanism. In other words, we must ensure that the static data is accessed by one thread at the same time only.

Usually, we achieve that using ‘lock’ keyword (C#). For instance, in an application we do have a cache manager class that stores some data in a memory cache shared by all the threads. If the cache manager has the data in its dictionary, it returns it otherwise; we will get it from the database and store it in the cache manager (this is for illustration only. Do not bother with the design).

The code would look like:

        public string RetrieveSingleContent(string key)
        {        
                string cacheValue = CacheManager.ReadFromCache(key);
                if (cacheValue != null)
                    return cacheValue;
                lock (_lockObjectSingleContent)
                {
                        using (IDataReader content = GetFromDB(key))
                        {
                            cacheValue = content.GetString(0);
                        }
                        CacheManager.AddToCache(key, cacheValue);
                }
               return cacheValue;
        }

What’s wrong with this code?

If we do have many threads requesting the same content (same key), we will end up with a queue of threads wanting to access the locked section of the code. Once we have retrieved the content from the database (first thread), the next thread will request the same data from the database even though the first thread did it already.

Checking the cacheValue before getting the database is not a solution either because cacheValue is a local variable and thus, it is not shared by all the threads. Therefore, the most efficient solution in the case is to query the content manager outside the lock section and also inside the lock section. In this case, whenever a queued thread will enter the section, it will first recheck whether the requested content has been retrieved in the meantime (between the moment it was waiting for its turn till it gets inside the locked section).

The new method will therefore look like:

        public override string RetrieveSingleContent(string key)
        {
                string cacheValue = CacheManager.ReadFromCache(key);
                if (cacheValue != null)
                    return cacheValue;
                lock (_lockObjectSingleContent)
                {
                    cacheValue = CacheManager.ReadFromCache(key);
                    if (cacheValue == null)
                    {
                        using (IDataReader content = GetFromDB(key))
                        {
                            cacheValue = content.GetString(0);
                        }
                        CacheManager.AddToCache(key, cacheValue);
                    }
                }
                return cacheValue;
        }

Obviously, if querying the cache manager for data is more expensing than getting the data from the database, then this will no longer be valid. But anyway, in this case, you should not use the cache at all.