Replies: 3 comments 3 replies
-
@stephentoub Curious if you have any comments on this matter, given that you were involved in the related issues. |
Beta Was this translation helpful? Give feedback.
-
@VSadov Do you have any thoughts here? |
Beta Was this translation helpful? Give feedback.
-
I was hit by this problem just recently and had to fix it myself by finding affected areas and changing the input to I realize this is not the most popular issue or problem in .NET, but when it hits, the results are scary. @stephentoub mentioning you since you seem most aware of this gimmick - perhaps it'd be wise to reconsider opt-out mechanism for concurrent collections from LINQ optimizations around them so we can fix this issue properly. Making concurrent collections implement something like |
Beta Was this translation helpful? Give feedback.
-
I figured I would start a discussion about this before creating an issue for it. As far as I know, adding DIMs to the BCL is something that may happen in the future but a conservative "wait for now" approach is being taken here since it is a relatively new feature that may have unintended consequences. Is this still the case?
The Problem
The BCL currently has 1 collection that is safe to enumerate over but NOT safe to call LINQ methods on and not safe to pass in as a
List
constructor parameter -ConcurrentDictionary
. I have several others that fall into the same category, namely a set of collections that store weak references to values and an alternate concurrent dictionary implementation that provides a proper point-in-time snapshot while being enumerated over.The only reason these aren't safe to use in LINQ methods and the
List
constructor is because they check if theIEnumerable
collection also implementsICollection
, and if so, assumes thatICollection.Count
can be called to determine the number of items in the collection prior to creating an array to hold the items. This creates a race condition between the call toCount
and the copying of the items to the resulting array.The fact that LINQ methods can't be called on something that states it is safe to enumerate due to hidden internal dynamic-downcasting optimizations can not only be surprising but also very difficult to debug while going unnoticed for a long time, resulting in infrequent random crashes, or even worse, junk-data silently making its way through the application and persisting to storage, only under high-load situations. I have no idea how I would find a bug caused by this after a client complained that a random
null
or0
value popped into their data once in a blue moon, and immense amounts of hair-pulling would ensue. I got lucky that I stumbled upon this issue before things went into production but many others may not be so lucky.If a type can be safely enumerated, then IMO it should be safe to use in LINQ and any other place in the BCL that accepts an
IEnumerable
parameter, and methods that dynamically down-cast toICollection
for optimization purposes should be responsible for ensuring those optimizations do not have hidden unintended consequences which break their contract. I have outlined 2 options below that would allow this to happen.Ultimately, it seems to me that
ConcurrentDictionary
probably should not have implementedICollection<T>
until an approach was available to safely allow it to do so without causing this problem. That's what I decided to do for now with our collections, but it would be nice to fix this so that we can implementICollection
and so other developers don't fall into this trap when usingConcurrentDictionary
.Related Issues
For further context, see the following issues:
Make IListProvider public
LINQ's Buffer ctor requires Count/CopyTo results be consistent
Immediate Mitigation
Prominent documentation needs to be added to
ConcurrentDictionary
to indicate it is NOT safe to pass into LINQ methods or any other method that accepts anIEnumerable<T>
parameter but optimizes forICollection<T>
, preferably both in the main class documentation and theGetEnumerator()
method documentation. The current docs forGetEnumerator()
contains this information:While this is strictly true, it lead me to believe that
ConcurrentDictionary
was safe to use with LINQ, which conceptually should work on anything that can be enumerated over. It would be nice to have some clearer information here.Solution 1: Add
ToArray()
(as a DIM) onICollection<T>
All the places I could find where this is a problem downcast the
IEnumerable
toICollection
, callCount
to create an array, then callCopyTo()
to copy the items. Having a single method that returns an array would eliminate the race condition between those two calls.ConcurrentDictionary
already has a safeToArray()
method which does the appropriate locking to safely return an array. It would be trivial to add the same functionality to my collection classes.This option is my preferred approach for performance reasons - no need for some kind of dictionary type lookup to check if the collection is concurrent, and collections can provide safe/optimized
ToArray()
implementations instead of forcing methods to fall back to unoptimizedIEnumerable
behavior.The default implementation of the method would just call
Count
and thenCopyTo()
, and concurrent collections would override this default implementation with their safe versions.Solution 2: Provide opt-out mechanism for
ICollection.Count
optimizationsThis could be something like a
[ConcurrentCollection]
attribute that can be placed on collection classes to indicate that collection state can change between method calls and thus doing something like callingCount
and thenCopyTo()
is not safe. A static helper method such asEnumerable.IsConcurrentCollection(Type collectionType)
could be provided which would cache whether a collection type is concurrent or not. If this returnstrue
then theICollection
optimizations would not be used and methods would fall back toIEnumerable
behavior.Another approach could be a marker interface like
IConcurrentCollection
which could be checked after a successfulICollection
cast to ensure it is safe to proceed with the optimization.Beta Was this translation helpful? Give feedback.
All reactions