6

ConcurrentDictionary.TryUpdate method requires the comparisonValue that is compared with the value of the element that has the specified key. But if I trying to do something like this:

if (!_store.TryGetValue(book.Id, out Book existing))
{
    throw new KeyNotFoundException();
}

if (!_store.TryUpdate(book.Id, book, existing))
{
    throw new Exception("Unable to update the book");
}

when multiple threads are updating one book at the same time it throws an exception, because existing book was changed in another thread.

I can't use indexer, because it adds the book if it not exists, and I can't check the key is exists because it also will not atomic.

I was changed my code like this:

while (true)
{
    if (!_store.TryGetValue(book.Id, out Book existing))
    {
        throw new KeyNotFoundException();
    }

    if (_store.TryUpdate(book.Id, book, existing))
    {
        break;
    }
}

but I'm worried about the infinite loop.

But if I will use locking on Update and Delete methods, I will lose the advantage of using ConcurrentDictionary.

What is the right way to solve my problem?

Siavash
  • 2,813
  • 4
  • 29
  • 42
  • 1
    *when multiple threads are updating one book at the same time it throws an exception, because existing book was changed in another thread.* Is that not the desired behavior? What are you actually trying to achieve here? – Matt Burland Oct 10 '18 at 17:03
  • 1
    If you just want to add or update without regard to whether or not another thread might have updated, then you need to use [AddOrUpdate](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.addorupdate?view=netframework-4.7.2) – Matt Burland Oct 10 '18 at 17:06
  • Try methods attempt to perform an action upon the contents of the collection, but return false if they cannot. The reason they cannot do something is usually because the given key is invalid. It may have been changed on another thread. – Eldho Oct 10 '18 at 17:06
  • @MattBurland, I want the latest changes to be saved regardless of whether someone else has changed them before –  Oct 10 '18 at 17:06
  • 2
    @Godzilla - then AddOrUpdate should do it – Matt Burland Oct 10 '18 at 17:07
  • I would worry more about the correctness of the code than the while loop. With this logic, whatever thread is last will win as far as what gets put into the dictionary, unless there is other logic you're not showing. You might as well just replace the TryUpdate with the indexer in your example above with the while loop – Ted Elliott Oct 10 '18 at 17:08
  • @MattBurland, adding a nonexistent key is not allowed under the conditions of the task –  Oct 10 '18 at 17:09
  • @TedElliott, the indexer also adds a key if it not exists or was removed by another thread –  Oct 10 '18 at 17:12
  • Well then do a `TryGetValue` (you don't actually care about the value) to see if it exists and then do a `AddOrUpdate` if `TryGetValue` returned true – Matt Burland Oct 10 '18 at 17:12
  • @MattBurland That's not atomic. Other changes can happen in between any operation you perform on it. – Servy Oct 10 '18 at 17:13
  • @Servy - I know, but they only care about the last update winning. – Matt Burland Oct 10 '18 at 17:13
  • @MattBurland They've specifically said they *do* care about it being atomic, several times, and specifically explained why your proposal is unacceptable. – Servy Oct 10 '18 at 17:15
  • @MattBurland, it's an interesting idea, but this does not exclude that between calls `TryGetValue` and `AddOrUpdate`, the book was removed –  Oct 10 '18 at 17:15
  • @Godzilla - if removals are a possibility, then no, that doesn't work. You will have to lock. However, _But if I will use locking on Update and Delete methods, I will lose the advantage of using ConcurrentDictionary._, I'm not sure what advnatage you _think_ `ConcurrentDictionary` has, but it achieves it thread-safety but _locking_ around critical functions. https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,7996972d3d91c9d0,references – Matt Burland Oct 10 '18 at 17:18
  • @Servy - read the comments: *I want the latest changes to be saved regardless of whether someone else has changed them before* They are not concerned with it being atomic _per se_, but believe that by making it atomic it will solve their problem. – Matt Burland Oct 10 '18 at 17:19
  • I thinking that your best bet might be to write a class that wraps an internal `ConcurrentDictionary` and exposes only the methods you need (Add, Remove, etc) and holds an additional lock to prevent removals during your operation which would be exposed as a separate method called (let's say) `UpdateIfExists`. – Matt Burland Oct 10 '18 at 17:27
  • @MattBurland Yes, I read the comments, including where they said that if they did what you proposed, someone could remove the item in between the operations, and then the result would be incorrect, which they mentioned multiple times. What they're stating their requirements as being *does* require atomicity. – Servy Oct 10 '18 at 17:28
  • @MattBurland There's no point in using a ConcurrentDictionary if you're just going to do all of your own locking around all of your operations. That's just locking redundantly. – Servy Oct 10 '18 at 17:29
  • About other logic - it's just a CRUD where the latest is right. And if he removes the value while you check for exists, he is right too –  Oct 10 '18 at 17:30
  • Actually there is, because there's a _lot_ going on in `ConcurrentDictionary` that would be tedious to recreate. You can defer _a lot_ of that to the underlying `ConcurrentDictionary` instead or reinventing the wheel. Given the scenario they have proposed, they only really need to make sure there are no deletions between checking a key exists and updating it. They could include additions too if that's an issue. – Matt Burland Oct 10 '18 at 17:31
  • @MattBurland That means you're exposing operations of the concurrent dictionary without locking, which now means the operations of yours that need to be atomic aren't. Locking around each of your critical sections that use the collection isn't particularly hard to do, and you need to do it regardless of what type the underlying collection is if you need operations ConcurrentDictionary doesn't provide to be atomic, so no, it's not getting you anything. – Servy Oct 10 '18 at 17:40

2 Answers2

1

It can be done by adding wrapper which will able to replace value. To simplify code I will implement this wrapper with locks (to avoid double value construction).

First of all - interface. Please check if it reflects operations needed. I used int type for keys and string for value just to simplify example.

    public delegate TValue GetNewValue<TValue>(TValue previousValue);

    public interface IIntStringAtomicDictionary
    {
        /// <returns>true if was added, otherwise false</returns>
        bool AddIfMissingOnly(int key, Func<string> valueGetter);

        /// <returns>true if was updated, otherwise false</returns>
        bool UpdateIfExists(int key, GetNewValue<string> convertPreviousValueToNew);
    }

Implementation is below. It could not remove value, it can be done simple (I can update answer if you need)

    public sealed class IntStringAtomicDictionary : IIntStringAtomicDictionary
    {
        private readonly ConcurrentDictionary<int, ValueWrapper<string>> _innerDictionary = new ConcurrentDictionary<int, ValueWrapper<string>>();
        private readonly Func<int, ValueWrapper<string>> _wrapperConstructor = _ => new ValueWrapper<string>();

        public bool AddIfMissingOnly(int key, Func<string> valueGetter)
        {
            var wrapper = _innerDictionary.GetOrAdd(key, _wrapperConstructor);

            return wrapper.AddIfNotExist(valueGetter);
        }

        public bool UpdateIfExists(int key, GetNewValue<string> convertPreviousValueToNew)
        {
            var wrapper = _innerDictionary.GetOrAdd(key, _wrapperConstructor);

            return wrapper.AddIfExists(convertPreviousValueToNew);
        }
    }

    private sealed class ValueWrapper<TValue> where TValue : class
    {
        private readonly object _lock = new object();
        private TValue _value;

        public bool AddIfNotExist(Func<TValue> valueGetter)
        {
            lock (_lock)
            {
                if (_value is null)
                {
                    _value = valueGetter();

                    return true;
                }

                return false;
            }
        }

        public bool AddIfExists(GetNewValue<TValue> updateValueFunction)
        {
            lock (_lock)
            {
                if (!(_value is null))
                {
                    _value = updateValueFunction(_value);

                    return true;
                }

                return false;
            }
        }
    }

After writing the code we can re-read requirements. As I understand, we have to apply the following:

  • Different keys should be updates from the different threads without locking.
  • Value update should be atomic
  • Parallel value adding if forbidden - please say if it is wrong
  • Different values should be able to be created from the different threads.

Because of "parallel value adding" restriction we have to lock value creation. Therefore my wrapper above has this lock.

All other operations are not use any locks.

Additional improvements:

  • ValueWrapper class can use ReadWriteLockSlim to allow value read in parallel.
  • Values can be removed with the same locks. Of course we can have race condition here.
Manushin Igor
  • 3,398
  • 1
  • 26
  • 40
-1

One possible solution is accepting null values:

if (!_store.AddorOUpdate(book.Id, null, (k, v) => book))
{
    throw new Exception("Unable to update the book");
}

It has some caveats though:

  • It is not enough that a key exists in order to know if a book is in the dictionary, but its value has to be not null.
  • The increase of the size of the dictionary with useless keys.
ldonoso
  • 171
  • 2
  • 6