4

If I have a ConcurrentDictionary instance, does it matter whether I use the Count property or LINQ's Any()? I'd rather write dict.Any() instead of dict.Count > 0 as I think Any() is more descriptive.

I'm only concerned about correctness, not performance. The use case is

void process()
{
   if (concurrentDictionary.Count <= 0) // or !Any() ?
      return; // dictionary is empty, nothing to do

   // ...
}
Ðаn
  • 10,934
  • 11
  • 59
  • 95
  • The question is why you are dealing with `Any` or `Count` in the first place. Those will give you a snapshot value which may, by the very nature of the whole situation, be void in the next instant. If you need a snapshot of the contents of the dictionary, use the `.ToArray` method of it, then you can analyze the contents of that. – Lasse V. Karlsen Apr 01 '15 at 19:49
  • In any case, it depends on what you define "the same as" in this context. Does it execute the same code? No. – Lasse V. Karlsen Apr 01 '15 at 19:55
  • 1
    Semantically, yes, they are the same. Performance wise, no, it is not guaranteed. You should always use `Any()` for the best performance. See: http://stackoverflow.com/a/691155/50776 – casperOne Apr 01 '15 at 20:45

3 Answers3

5

The question Are IEnumerable Linq methods thread-safe? addresses the fact that the IEnumerable methods on LINQ queries are not thread safe without a specific lock being kept protecting the collection.

You can look at the reference code for ConcurrentDictionary to see that the enumerator does not provide a thread-safe snapshot. Additional the MSDN Documentation for ConcurrentDictionary.GetEnumerator states:

The enumerator returned from the dictionary is safe to use concurrently with reads and writes to the dictionary, however it does not represent a moment-in-time snapshot of the dictionary. The contents exposed through the enumerator may contain modifications made to the dictionary after GetEnumerator was called

The Count property takes a full lock on the dictionary and returns a consistent result.

So depending on whether you want to take a lock on the dictionary to run Any() it is probably cleaner to check for Count > 0.

Community
  • 1
  • 1
Steve Mitcham
  • 5,268
  • 1
  • 28
  • 56
  • wrong: [ConcurrentDictionary.GetEnumerator](https://msdn.microsoft.com/en-us/library/dd287131%28v=vs.110%29.aspx): *The enumerator returned from the dictionary **is safe to use concurrently with reads and writes to the dictionary**, however it does not represent a moment-in-time snapshot of the dictionary. The contents exposed through the enumerator may contain modifications made to the dictionary after GetEnumerator was called.* – xanatos Apr 01 '15 at 19:59
  • That is what I thought I said. – Steve Mitcham Apr 01 '15 at 19:59
  • 1
    @SteveMitcham I think the key is that "are not thread safe" - not entirely correct. They are safe but not provide operation over some consistent state. – zerkms Apr 01 '15 at 20:02
  • 1
    *you can see that the enumerator that is returned is not thread safe* It depends on what you mean with thread safe. It won't corrupt data. But it can return "false" results... But then, when the method returns, the result could be falsified by other threads, so the difference is minimal. – xanatos Apr 01 '15 at 20:02
  • I mean what you meant, but you said it much more clearly than me. – Steve Mitcham Apr 01 '15 at 20:04
  • @xanatos Edited to clarify the meaning of thread safe and link in the documentation from your comment. – Steve Mitcham Apr 01 '15 at 20:07
2

You will have to benchmark them, because the Any() is something like

using (IEnumerator<TSource> enumerator = source.GetEnumerator())
{
    if (enumerator.MoveNext())
    {
        return true;
    }
}

return false;

so it requires enumeration, that for ConcurrentDictionary is something complex, but even the Count of ConcurrentDictionary isn't cached and it seems to be pretty complex.

I'll add that the Count must still traverse some internal structures (as in an array of) aquiring a lock on the whole dictionary, while the Any() will stop at the first non-empty bucket. I'll say that for a big dictionary, Count is slower, while for a small one it is faster.

Correction: the Count aquires a lock on all the dictionary before counting. it does call this.AcquireAllLocks().

Remember that the result of both method could be falsified before the methods return, because hey... concurrency! :-)

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • Did you check the sources or was it's an incredibly precise guess? Just in case: https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Enumerable.cs#L1425 – zerkms Apr 01 '15 at 19:57
  • 1
    @zerkms A third one... ILSpy... Still nearly equivalent to the first one. – xanatos Apr 01 '15 at 19:58
1

does it matter whether I use the Count property or LINQ's Any()

No. They are functionally the same and should have very little performance difference. Use whatever conveys the meaning most appropriately and only change it if there is a performance problem significant to the performance of the entire application.

Count will count the items in the dictionary at the moment that the property is called.

Any will call ConcurrentDictionary.GenEnumerator() to see if the dictionary has any items. According to the documentation, the enumerator returned will reflect and changes made to the dictionary after GetEnumerator() is called.

So it it theoretically possible that they count get different answers if an item was added between the time that Any is called and MoveNext is called within Any. However that time window should be so short the likelyhood should be very small.

Plus who's to say which is correct? If you can Any and Count at exactly the same time that an item is added, is the collection empty or not?

D Stanley
  • 149,601
  • 11
  • 178
  • 240
  • "However that time window should be so short the likelyhood should be very small" - you've obviously never written any working concurrent code. This para should read "that time window should be so short the likelyhood should only happen several times a day". – gbjbaanb Jun 17 '18 at 18:25