Skip to content

Commit 4fd889e

Browse files
authored
LRU: atomic ConcurrentDictionary.Remove() (#67)
* atomic remove
1 parent c1a9aba commit 4fd889e

File tree

2 files changed

+65
-55
lines changed

2 files changed

+65
-55
lines changed

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,19 @@ public void WhenKeyExistsTryUpdateUpdatesValueAndReturnsTrue()
385385
value.Should().Be("2");
386386
}
387387

388+
[Fact]
389+
public void WhenKeyExistsTryUpdateDisposesOldValue()
390+
{
391+
var lruOfDisposable = new ConcurrentLru<int, DisposableItem>(1, 6, EqualityComparer<int>.Default);
392+
var disposableValueFactory = new DisposableValueFactory();
393+
var newValue = new DisposableItem();
394+
395+
lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create);
396+
lruOfDisposable.TryUpdate(1, newValue);
397+
398+
disposableValueFactory.Items[1].IsDisposed.Should().BeTrue();
399+
}
400+
388401
[Fact]
389402
public void WhenKeyDoesNotExistTryUpdateReturnsFalse()
390403
{
@@ -403,7 +416,7 @@ public void WhenKeyDoesNotExistAddOrUpdateAddsNewItem()
403416
}
404417

405418
[Fact]
406-
public void WhenKeyExistsAddOrUpdatUpdatesExistingItem()
419+
public void WhenKeyExistsAddOrUpdateUpdatesExistingItem()
407420
{
408421
lru.AddOrUpdate(1, "1");
409422
lru.AddOrUpdate(1, "2");
@@ -412,6 +425,19 @@ public void WhenKeyExistsAddOrUpdatUpdatesExistingItem()
412425
value.Should().Be("2");
413426
}
414427

428+
[Fact]
429+
public void WhenKeyExistsAddOrUpdateDisposesOldValue()
430+
{
431+
var lruOfDisposable = new ConcurrentLru<int, DisposableItem>(1, 6, EqualityComparer<int>.Default);
432+
var disposableValueFactory = new DisposableValueFactory();
433+
var newValue = new DisposableItem();
434+
435+
lruOfDisposable.GetOrAdd(1, disposableValueFactory.Create);
436+
lruOfDisposable.AddOrUpdate(1, newValue);
437+
438+
disposableValueFactory.Items[1].IsDisposed.Should().BeTrue();
439+
}
440+
415441
[Fact]
416442
public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder()
417443
{

BitFaster.Caching/Lru/TemplateConcurrentLru.cs

Lines changed: 38 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)