Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GetOrAdd Concurrency Problem #268

Open
tippmar opened this issue Jan 22, 2019 · 6 comments
Open

GetOrAdd Concurrency Problem #268

tippmar opened this issue Jan 22, 2019 · 6 comments
Labels

Comments

@tippmar
Copy link

tippmar commented Jan 22, 2019

I'm finding that, when multiple threads call .GetOrAdd() on the same empty ICacheManager<T> instance with the same key value, that the valueFactory func is called multiple times, indicating an apparent lack of any synchronization within .GetOrAdd().

Is this the correct behavior? My expectation is that the first thread to call .GetOrAdd() when the given key does not exist in cache would block other callers asking for the same key until the valueFactory func is complete and then return that newly cached item.

@MichaCo
Copy link
Owner

MichaCo commented Jan 22, 2019

There is currently no locking mechanism on keys, no.
So yeah, that's the expected behavior if you run multiple threads for the same key, you might get collisions.

That being said, I'm planning to add lock helpers and some functionality in the future which will help with that.

@tippmar
Copy link
Author

tippmar commented Jan 22, 2019

Any suggestions for a short-term workaround? Or a timeline on adding the locking semantics?

@MichaCo
Copy link
Owner

MichaCo commented Jan 22, 2019

As long as you don't use distributed caching, it is relatively easy.
You can cache SemaphoreSlims per key. Lock on that and release and remove the semaphore afterwards.
If you do use distributed caching, you would have to use a distributed lock mechanism which can get way more complicated.

@pmunin
Copy link

pmunin commented Sep 28, 2019

Just to support with missing feature with test case:

        [Fact]
        public void TestConcurrentGetOrAddSameKey()
        {
            var cache = CacheFactory.Build<object>(settings => settings
                .WithMicrosoftMemoryCacheHandle()
                .And
                //.WithUpdateMode(CacheUpdateMode.Up)
                .WithGzJsonSerializer()
                );

            CacheItem<object> GenerateValue(string key)
            {
                Thread.Sleep(5000);
                return new CacheItem<object>(key, $"{rnd.Next()} {DateTime.Now.ToLongTimeString()} : HALLO WORLD FOR " + key);
            }

            object v1 = null;
            object v2 = null;

            const string sameKey = "Test 1 ";

            Parallel.Invoke(
                () => { v1 = cache.GetOrAdd(sameKey, GenerateValue).Value; },
                () => { v2 = cache.GetOrAdd(sameKey, GenerateValue).Value; }
            );

            Assert.True(v1 == v2); // FAILS
        }

@pmunin
Copy link

pmunin commented Sep 28, 2019

@MichaCo shouldn't it be ReaderWriterLockSlim rather than SemaphoreSlim?

@tippmar
Copy link
Author

tippmar commented Oct 16, 2019

In case anyone else comes upon this issue and wants a workaround, I've built a set of extension methods that provide synchronized behavior for .GetOrAdd() -- the only added requirement for using the extensions is that you have to pass in the synchronization object you want to use - typically, a statically declared Object() instance, or, in the case of the async extension, a statically declared AsyncLock() instance (which requires installation of the Nito.AsyncEx package).

Hope this helps someone....

public static class ICacheManagerExtensions
    {
        /// <summary>
        /// A synchronized version of <see cref="ICacheManager{TCacheValue}>"/> GetOrAdd that guarantees that the valueFactory function
        /// will be invoked exactly one time, blocking all other simultaneous callers until the factory func is complete.
        ///
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cacheManager"></param>
        /// <param name="lockObject">a static <see cref="Object"/> instance, declared at the same scope as cacheManager, that will be used to synchronize access to the cache manager</param>
        /// <param name="cacheKey"></param>
        /// <param name="valueFactory"></param>
        /// <returns></returns>
        public static T SynchonrizedGetOrAdd<T>(this ICacheManager<T> cacheManager, object lockObject, string cacheKey, Func<string, T> valueFactory)
        {
            if (!cacheManager.Exists(cacheKey))
            {
                lock (lockObject)
                {
                    if (!cacheManager.Exists(cacheKey))
                    {
                        var value = valueFactory(cacheKey);

                        cacheManager.Add(cacheKey, value);
                        return value;
                    }
                }
            }

            return cacheManager.Get(cacheKey);
        }

        /// <summary>
        /// A synchronized, async version of <see cref="ICacheManager{TCacheValue}>"/> GetOrAdd that guarantees that the async valueFactory function
        /// will be invoked exactly one time, blocking all other simultaneous callers until the factory func is complete.
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cacheManager"></param>
        /// <param name="lockObject">a static <see cref="AsyncLock"/> instance, declared at the same scope as cacheManager, that will be used to synchronize access to the cache manager</param>
        /// <param name="cacheKey"></param>
        /// <param name="asyncValueFactory"></param>
        /// <returns></returns>
        public static async Task<T> SynchronizedGetOrAddAsync<T>(this ICacheManager<T> cacheManager, AsyncLock lockObject, string cacheKey, Func<string, Task<T>> asyncValueFactory)
        {
            if (!cacheManager.Exists(cacheKey))
            {
                using (await lockObject.LockAsync())
                {
                    if (!cacheManager.Exists(cacheKey))
                    {
                        var value = await asyncValueFactory(cacheKey);

                        cacheManager.Add(cacheKey, value);
                        return value;
                    }
                }
            }

            return cacheManager.Get(cacheKey);
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants