Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-blocking ConcurrentDictionary #50337

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@
<Compile Include="System\Collections\Concurrent\CDSCollectionETWBCLProvider.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentBag.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary.cs" />

<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImpl.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImpl.SnapshotImpl.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImpl`2.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImpl`3.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImplBoxed.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImplInt.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImplLong.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImplNint.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\DictionaryImplRef.cs" />

<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\Counter\CounterBase.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\Counter\Counter32.cs" />
<Compile Include="System\Collections\Concurrent\ConcurrentDictionary\Counter\Counter64.cs" />

<Compile Include="System\Collections\Concurrent\ConcurrentStack.cs" />
<Compile Include="System\Collections\Concurrent\OrderablePartitioner.cs" />
<Compile Include="System\Collections\Concurrent\Partitioner.cs" />
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;

#nullable disable

namespace System.Collections.Concurrent
{
/// <summary>
/// Scalable 32bit counter that can be used from multiple threads.
/// </summary>
internal sealed class Counter32: CounterBase
{
private class Cell
{
[StructLayout(LayoutKind.Explicit, Size = CACHE_LINE * 2 - OBJ_HEADER_SIZE)]
public struct SpacedCounter
{
[FieldOffset(CACHE_LINE - OBJ_HEADER_SIZE)]
public int count;
}

public SpacedCounter counter;
}

// spaced out counters
private Cell[] cells;

// default counter
private int count;

// delayed estimated count
private int lastCount;

/// <summary>
/// Initializes a new instance of the <see
/// cref="Counter32"/>
/// </summary>
public Counter32()
{
}

/// <summary>
/// Returns the value of the counter at the time of the call.
/// </summary>
/// <remarks>
/// The value may miss in-progress updates if the counter is being concurrently modified.
/// </remarks>
public int Value
{
get
{
var count = this.count;
var cells = this.cells;

if (cells != null)
{
for (int i = 0; i < cells.Length; i++)
{
var cell = cells[i];
if (cell != null)
{
count += cell.counter.count;
}
else
{
break;
}
}
}

return count;
}
}

/// <summary>
/// Returns the approximate value of the counter at the time of the call.
/// </summary>
/// <remarks>
/// EstimatedValue could be significantly cheaper to obtain, but may be slightly delayed.
Copy link
Member

@sharwell sharwell May 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Is 'stale' a better word choice here? Concern is the reader could interpret "slightly delayed" to mean "slower to return". Stale has its own downsides (e.g. is it eventually consistent?) so would leave the call to y'all.

Suggested change
/// EstimatedValue could be significantly cheaper to obtain, but may be slightly delayed.
/// EstimatedValue could be significantly cheaper to obtain, but may be stale.

/// </remarks>
public int EstimatedValue
{
get
{
if (this.cells == null)
{
return this.count;
}

var curTicks = (uint)Environment.TickCount;
// more than a millisecond passed?
if (curTicks != lastCountTicks)
{
lastCountTicks = curTicks;
lastCount = Value;
}

return lastCount;
}
}

/// <summary>
/// Increments the counter by 1.
/// </summary>
public void Increment()
{
int curCellCount = this.cellCount;
var drift = increment(ref GetCountRef(curCellCount));

if (drift != 0)
{
TryAddCell(curCellCount);
}
}

/// <summary>
/// Decrements the counter by 1.
/// </summary>
public void Decrement()
{
int curCellCount = this.cellCount;
var drift = decrement(ref GetCountRef(curCellCount));

if (drift != 0)
{
TryAddCell(curCellCount);
}
}

/// <summary>
/// Increments the counter by 'value'.
/// </summary>
public void Add(int value)
{
int curCellCount = this.cellCount;
var drift = add(ref GetCountRef(curCellCount), value);

if (drift != 0)
{
TryAddCell(curCellCount);
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref int GetCountRef(int curCellCount)
{
ref var countRef = ref count;

Cell[] cells;
if ((cells = this.cells) != null && curCellCount > 1)
{
var cell = cells[GetIndex((uint)curCellCount)];
if (cell != null)
{
countRef = ref cell.counter.count;
}
}

return ref countRef;
}

private static int increment(ref int val)
{
return -val - 1 + Interlocked.Increment(ref val);
}

private static int add(ref int val, int inc)
{
return -val - inc + Interlocked.Add(ref val, inc);
}

private static int decrement(ref int val)
{
return val - 1 - Interlocked.Decrement(ref val);
}

private void TryAddCell(int curCellCount)
{
if (curCellCount < s_MaxCellCount)
{
TryAddCellCore(curCellCount);
}
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void TryAddCellCore(int curCellCount)
{
var cells = this.cells;
if (cells == null)
{
var newCells = new Cell[s_MaxCellCount];
cells = Interlocked.CompareExchange(ref this.cells, newCells, null) ?? newCells;
}

if (cells[curCellCount] == null)
{
Interlocked.CompareExchange(ref cells[curCellCount], new Cell(), null);
}

if (this.cellCount == curCellCount)
{
Interlocked.CompareExchange(ref this.cellCount, curCellCount + 1, curCellCount);
}
}
}
}
Loading