问题
I have a ConcurrentDictionary which maps a simple type to a list:
var dict = new ConcurrentDictionary<string, List<string>>();
I can use AddOrUpdate() to cater for both initialization of the list when the first value is added, and addition of subsequent values to the list.
However, the same isn't true for removal. If I do something like:
public void Remove(string key, string value)
{
List<string> list;
var found = dict.TryGetValue(key, out list);
if (found)
{
list.Remove(value);
if (list.Count == 0)
{
// warning: possible race condition here
dict.TryRemove(key, out list);
}
}
}
...where my intention is to remove the key completely if the corresponding list no longer has any values (similar to reference counting, in concept), then I'm risking a race condition because someone might have added something to the list right after I checked whether it's empty.
Although I am using a list in this simple example, I usually have a ConcurrentBag or ConcurrentDictionary in such scenarios, and the risk is quite similar.
Is there any way of safely removing a key when the corresponding collection is empty, short of resorting to locks?
回答1:
Your ConcurrentDictionary
is protected but your list is not. If your list can be accessed from multiple threads (I assume this is the case) you need to use locking around all accesses to the list or you need to use a different construct.
After you call TryGetValue
in your Remove
function, you access the list multiple times - since List<T>
is not safe for multithreading, you run the risk of various threading issues.
If you use nested ConcurrentDictionaries in dict
, you only run the problem of removing something that's not empty - as you wrote, it's possible that an item is added to the nested ConcurrentDictionary after you check its size. Removing the nested list / dictionary itself is thread-safe: the containing dict
is a ConcurrentDictionary
and it'll handle removing items safely. However, if you want to guarantee that a list/dictionary is only removed if it's empty, you have to use a lock around the whole operation.
This is because the container dict
and the nested list/dictionary are two different constructs and touching one has no effect on the other - if you need the entire multi-step operation to be atomic, you'll have to make sure only one thread can attempt to do it at a time.
Your code would be something like this:
if (found)
{
lock ( _listLock )
{
list.Remove(value);
if (list.Count == 0)
{
// warning: possible race condition here
dict.TryRemove(key, out list);
}
}
}
Again, if you're using an unprotected construct (like a List<T>
then you have to use locking around every access to that list.
来源:https://stackoverflow.com/questions/27297326/safely-removing-list-mapping-from-concurrentdictionary