@@ -176,48 +176,34 @@ public async Task<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
176
176
///<inheritdoc/>
177
177
public bool TryRemove ( K key )
178
178
{
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.
190
179
if ( this . dictionary . TryGetValue ( key , out var existing ) )
191
180
{
192
- if ( existing . WasRemoved )
193
- {
194
- return false ;
195
- }
181
+ var kvp = new KeyValuePair < K , I > ( key , existing ) ;
196
182
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 ) )
208
186
{
209
187
// Mark as not accessed, it will later be cycled out of the queues because it can never be fetched
210
188
// from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled
211
189
// from the queue.
212
- removedItem . WasAccessed = false ;
190
+ existing . WasAccessed = false ;
191
+ existing . WasRemoved = true ;
213
192
214
- if ( removedItem . Value is IDisposable d )
193
+ // serialize dispose (common case dispose not thread safe)
194
+ lock ( existing )
215
195
{
216
- d . Dispose ( ) ;
196
+ if ( existing . Value is IDisposable d )
197
+ {
198
+ d . Dispose ( ) ;
199
+ }
217
200
}
218
201
219
202
return true ;
220
203
}
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 ) ;
221
207
}
222
208
223
209
return false ;
@@ -233,7 +219,14 @@ public bool TryUpdate(K key, V value)
233
219
{
234
220
if ( ! existing . WasRemoved )
235
221
{
222
+ V oldValue = existing . Value ;
236
223
existing . Value = value ;
224
+
225
+ if ( oldValue is IDisposable d )
226
+ {
227
+ d . Dispose ( ) ;
228
+ }
229
+
237
230
return true ;
238
231
}
239
232
}
@@ -247,16 +240,9 @@ public bool TryUpdate(K key, V value)
247
240
public void AddOrUpdate ( K key , V value )
248
241
{
249
242
// 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 ;
260
246
}
261
247
262
248
// then try add
@@ -380,26 +366,24 @@ private void Move(I item, ItemDestination where)
380
366
Interlocked . Increment ( ref this . coldCount ) ;
381
367
break ;
382
368
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
+
386
378
lock ( item )
387
379
{
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 )
394
381
{
395
- item . WasRemoved = true ;
396
- if ( removedItem . Value is IDisposable d )
397
- {
398
- d . Dispose ( ) ;
399
- }
400
- }
382
+ d . Dispose ( ) ;
383
+ }
401
384
}
402
385
}
386
+
403
387
break ;
404
388
}
405
389
}
0 commit comments