9

So I have a IList as the value in my ConcurrentDictionary.

ConcurrentDictionary<int, IList<string>> list1 = new ConcurrentDictionary<int, IList<string>>;

In order to update a value in a list I do this:

if (list1.ContainsKey[key])
{
  IList<string> templist;
  list1.TryGetValue(key, out templist);
  templist.Add("helloworld");
}

However, does adding a string to templist update the ConcurrentDictionary? If so, is the update thread-safe so that no data corruption would occur?

Or is there a better way to update or create a list inside the ConcurrentDictionary

EDIT

If I were to use a ConcurrentBag instead of a List, how would I implement this? More specifically, how could I update it? ConcurrentDictionary's TryUpdate method feels a bit excessive.

Does ConcurrentBag.Add update the ConcurrentDictionary in a thread-safe mannar?

ConcurrentDictionary<int, ConcurrentBag<string>> list1 = new ConcurrentDictionary<int, ConcurrentBag<string>>

7 Answers7

8

Firstly, there's no need to do ContainsKey() and TryGetValue().

You should just do this:

IList<string> templist;

if (list1.TryGetValue(key, out templist))
    templist.Add("helloworld");

In fact your code as written has a race condition.

Inbetween one thread calling ContainsKey() and TryGetValue() a different thread may have removed the item with that key. Then TryGetValue() will return tempList as null, and then you'll get a null reference exception when you call tempList.Add().

Secondly, yes: There's another possible threading issue here. You don't know that the IList<string> stored inside the dictionary is threadsafe.

Therefore calling tempList.Add() is not guaranteed to be safe.

You could use ConcurrentQueue<string> instead of IList<string>. This is probably going to be the most robust solution.

Note that simply locking access to the IList<string> wouldn't be sufficient.

This is no good:

if (list1.TryGetValue(key, out templist))
{
    lock (locker)
    {
        templist.Add("helloworld");
    }
}

unless you also use the same lock everywhere else that the IList may be accessed. This is not easy to achieve, hence it's better to either use a ConcurrentQueue<> or add locking to this class and change the architecture so that no other threads have access to the underlying IList.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
5

Operations on a thread-safe dictionary are thread-safe by key, so to say. So as long as you access your values (in this case an IList<T>) only from one thread, you're good to go.

The ConcurrentDictionary does not prevent two threads at the same time to access the value beloning to one key.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
5

You can use ConcurrentDictionary.AddOrUpdate method to add item to list in thread-safe way. Its simpler and should work fine.

var list1 = new ConcurrentDictionary<int, IList<string>>();
list1.AddOrUpdate(key, 
      new List<string>() { "test" }, (k, l) => { l.Add("test"); return l;});

UPD

According to docs and sources, factories, which was passed to AddOrUpdate method will be run out of lock scope, so calling List methods inside factory delegate is NOT thread safe.

See comments under this answer.

tym32167
  • 4,741
  • 2
  • 28
  • 32
  • 1
    Alright, thanks for that. Now to comment on your actual implementation: that will still cause threading issues. What if another thread is iterating over the list at the moment `AddOrUpdate()` is called? – CodeCaster Jan 11 '17 at 13:38
  • @CodeCaster Obviously, if we are using non thread safe data structure in multithread env, this is not safe. Bu I thought that question is about adding value to list in multithreaded way. If OP want to get access to list in multithreaded way, he should use multithreaded data structure. – tym32167 Jan 11 '17 at 13:40
  • The changes to the list are still not safe when doing this. This code can still end up mutating the list from multiple threads at the same time, with all sorts of problematic results possible. – Servy Jan 11 '17 at 15:17
  • @tym32167 If you know that accessing the list isn't safe, then *don't say that this is safe*. Clearly this is being used in a multithreaded context, given that the whole point of the question is asking if this is safe in a multithreaded context. – Servy Jan 11 '17 at 15:18
  • @Servy my piece of code is safe in multithreaded context. It is not safe to use my code and try to modify same list in other thread. Why should I explain to someone that using non-safe List in multithreaded env is not safe? This is obvious. – tym32167 Jan 11 '17 at 18:32
  • 1
    @tym32167 No, your code is not safe. Your code is potentially accessing a list from multiple threads, and can break. – Servy Jan 11 '17 at 19:13
  • In documentation it is stated that "`updateValueFactory` delegate is called outside the locks to avoid the problems that can arise from executing unknown code under a lock." so updating like this is not thread safe. – M.kazem Akhgary Jan 21 '19 at 09:50
  • see remarks section https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2.addorupdate?view=netframework-4.7.2 – M.kazem Akhgary Jan 21 '19 at 09:51
  • @M.kazemAkhgary thanks for reminding me this, yep, now I realizing what was problem (it easier just to look at [this](https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,1206)). My answer is not thread safe, I will remove it later or will keep it just for history. – tym32167 Jan 21 '19 at 10:25
  • I think its good to keep it or maybe update it to indicate this is wrong and put correct usage if you want. I think its a common mistake (I made that mistake too) – M.kazem Akhgary Jan 21 '19 at 12:12
  • @M.kazemAkhgary correct usage already present in another answer, but I updated mine with reference to comments, thx. – tym32167 Jan 21 '19 at 12:21
  • These comments are highly recommended to read. I made this mistake too. – Vamos Jul 21 '21 at 10:34
2

The ConcurrentDictionary has no effect on whether you can apply changes to value objects in a thread-safe manner or not. That is the reponsiblity of the value object (the IList-implementation in your case).

Looking at the answers of No ConcurrentList<T> in .Net 4.0? there are some good reasons why there is no ConcurrentList implementation in .net.

Basically you have to take care of thread-safe changes yourself. The most simple way is to use the lock operator. E.g.

lock (templist)
{
   templist.Add("hello world");
}

Another way is to use the ConcurrentBag in the .net Framework. But this way is only useful for you, if you do not rely on the IList interface and the ordering of items.

Community
  • 1
  • 1
Ralf Bönning
  • 14,515
  • 5
  • 49
  • 67
  • I updated the question. Does updating the Bag update the Dictionary? –  Jan 11 '17 at 15:17
2

it has been already mentioned about what would be the best solution ConcurrentDictionary with ConcurrentBag. Just going to add how to do that

 ConcurrentBag<string> bag= new ConcurrentBag<string>();
 bag.Add("inputstring"); 
 list1.AddOrUpdate(key,bag,(k,v)=>{
     v.Add("inputString");
     return v;
 });
rkmorgan
  • 487
  • 4
  • 12
0

does adding a string to templist update the ConcurrentDictionary?

It does not.

Your thread safe collection (Dictionary) holds references to non-thread-safe collections (IList). So changing those is not thread safe.

I suppose you should consider using mutexes.

Mike
  • 9
  • 2
  • You're answering a different question than you quoted. Contrary to your answer, the update to the temp list *is* observable from other contexts getting the list from the dictionary, and that's precisely why it's not safe to do so. – Servy Jan 11 '17 at 15:21
  • "Is observable" does not mean it is updated. The dictionary is not changed as an object in any way. It holds (!) references to non thread safe collections, so of course any changes are observable. But that is also the reason why it is not safe. – Mike Jan 17 '17 at 07:56
  • And based on their later clarification, they clearly were asking if the change is observable through the concurrent dictionary. And just saying, "no" given the question asked, without clarifying it to mean that you're changing an object the dictionary references, rather than the dictionary itself, is still highly misleading in context. – Servy Jan 17 '17 at 14:18
0

If you use ConcurrentBag<T>:

var dic = new ConcurrentDictionary<int, ConcurrentBag<string>>();

Something like this could work OK:

public static class ConcurentDictionaryExt
{
    public static ConcurrentBag<V> AddToInternal<K, V>(this ConcurrentDictionary<K, ConcurrentBag<V>> dic, K key, V value)
        => dic.AddOrUpdate(key,
            k => new ConcurrentBag<V>() { value },
            (k, existingBag) =>
                {
                    existingBag.Add(value);
                    return existingBag;
                }
            );

    public static ConcurrentBag<V> AddRangeToInternal<K, V>(this ConcurrentDictionary<K, ConcurrentBag<V>> dic, K key, IEnumerable<V> values)
        => dic.AddOrUpdate(key,
            k => new ConcurrentBag<V>(values),
            (k, existingBag) =>
                {
                    foreach (var v in values)
                        existingBag.Add(v);
                    return existingBag;
                }
            );
}

I didn't test it yet :)

watbywbarif
  • 6,487
  • 8
  • 50
  • 64