@@ -176,48 +176,34 @@ public async Task<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
176176 ///<inheritdoc/>
177177 public bool TryRemove ( K key )
178178 {
179- // Possible race condition:
180- // Thread A TryRemove(1), removes LruItem1, has reference to removed item but not yet marked as removed
181- // Thread B GetOrAdd(1) => Adds LruItem1*
182- // Thread C GetOrAdd(2), Cycle, Move(LruItem1, Removed)
183- //
184- // Thread C can run and remove LruItem1* from this.dictionary before Thread A has marked LruItem1 as removed.
185- //
186- // In this situation, a subsequent attempt to fetch 1 will be a miss. The queues will still contain LruItem1*,
187- // and it will not be marked as removed. If key 1 is fetched while LruItem1* is still in the queue, there will
188- // be two queue entries for key 1, and neither is marked as removed. Thus when LruItem1 * ages out, it will
189- // incorrectly remove 1 from the dictionary, and this cycle can repeat.
190179 if ( this . dictionary . TryGetValue ( key , out var existing ) )
191180 {
192- if ( existing . WasRemoved )
193- {
194- return false ;
195- }
181+ var kvp = new KeyValuePair < K , I > ( key , existing ) ;
196182
197- lock ( existing )
198- {
199- if ( existing . WasRemoved )
200- {
201- return false ;
202- }
203-
204- existing . WasRemoved = true ;
205- }
206-
207- if ( this . dictionary . TryRemove ( key , out var removedItem ) )
183+ // hidden atomic remove
184+ // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
185+ if ( ( ( ICollection < KeyValuePair < K , I > > ) this . dictionary ) . Remove ( kvp ) )
208186 {
209187 // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched
210188 // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled
211189 // from the queue.
212- removedItem . WasAccessed = false ;
190+ existing . WasAccessed = false ;
191+ existing . WasRemoved = true ;
213192
214- if ( removedItem . Value is IDisposable d )
193+ // serialize dispose (common case dispose not thread safe)
194+ lock ( existing )
215195 {
216- d . Dispose ( ) ;
196+ if ( existing . Value is IDisposable d )
197+ {
198+ d . Dispose ( ) ;
199+ }
217200 }
218201
219202 return true ;
220203 }
204+
205+ // it existed, but we couldn't remove - this means value was replaced afer the TryGetValue (a race), try again
206+ return TryRemove ( key ) ;
221207 }
222208
223209 return false ;
@@ -233,7 +219,14 @@ public bool TryUpdate(K key, V value)
233219 {
234220 if ( ! existing . WasRemoved )
235221 {
222+ V oldValue = existing . Value ;
236223 existing . Value = value ;
224+
225+ if ( oldValue is IDisposable d )
226+ {
227+ d . Dispose ( ) ;
228+ }
229+
237230 return true ;
238231 }
239232 }
@@ -247,16 +240,9 @@ public bool TryUpdate(K key, V value)
247240 public void AddOrUpdate ( K key , V value )
248241 {
249242 // first, try to update
250- if ( this . dictionary . TryGetValue ( key , out var existing ) )
251- {
252- lock ( existing )
253- {
254- if ( ! existing . WasRemoved )
255- {
256- existing . Value = value ;
257- return ;
258- }
259- }
243+ if ( this . TryUpdate ( key , value ) )
244+ {
245+ return ;
260246 }
261247
262248 // then try add
@@ -380,26 +366,24 @@ private void Move(I item, ItemDestination where)
380366 Interlocked . Increment ( ref this . coldCount ) ;
381367 break ;
382368 case ItemDestination . Remove :
383- if ( ! item . WasRemoved )
384- {
385- // avoid race where 2 threads could remove the same key - see TryRemove for details.
369+
370+ var kvp = new KeyValuePair < K , I > ( item . Key , item ) ;
371+
372+ // hidden atomic remove
373+ // https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
374+ if ( ( ( ICollection < KeyValuePair < K , I > > ) this . dictionary ) . Remove ( kvp ) )
375+ {
376+ item . WasRemoved = true ;
377+
386378 lock ( item )
387379 {
388- if ( item . WasRemoved )
389- {
390- break ;
391- }
392-
393- if ( this . dictionary . TryRemove ( item . Key , out var removedItem ) )
380+ if ( item . Value is IDisposable d )
394381 {
395- item . WasRemoved = true ;
396- if ( removedItem . Value is IDisposable d )
397- {
398- d . Dispose ( ) ;
399- }
400- }
382+ d . Dispose ( ) ;
383+ }
401384 }
402385 }
386+
403387 break ;
404388 }
405389 }
0 commit comments