I would appreciate review of this implementation of the concurrent list that is, to my knowledge, currently missing in .NET Core.
Requirements
- Thread safe
- Lock free
- Ordered
- Supports get and set by index
- Implements generic IList
- Acceptable performance and time complexity compared to regular
IList
The main idea
- To keep order I use
ConcurrentQueue
- To get and set values at index I use
ConcurrentDictionary
- To make sure that both data sets are in sync I update them on Add, Remove
- To avoid excessive syncing, enumerator combines iteration over Queue + picks Dictionary if available
Questions
- Is there anything wrong with the code below, e.g. do I need to make a copy of enumerator before accessing it to ensure thread safety?
- Any concerns in regard to shallow copies and immutability?
- Any code samples or other implementations that perform better?
items.GetEnumerator() // Should it be items.ToList().GetEnumerator()
Excerpt from implementation
public class ConcurrentList<T> : IList<T>
{
protected ConcurrentQueue<T> _items = new();
protected ConcurrentDictionary<int, T> _indices = new();
public T this[int index]
{
get => _indices.TryGetValue(index, out T value) ? value : default;
set => UpdateInRange(index, 0, _items.Count, () => _indices[index] = value);
}
public int Count => _items.Count;
public bool IsReadOnly => false;
public void Add(T input) => _items.Enqueue(input);
public bool Contains(T input) => _items.Contains(input);
public void CopyTo(T[] items, int index) => _items.CopyTo(items, index);
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
public int IndexOf(T input) => _items.ToList().IndexOf(input);
public void Clear()
{
_items.Clear();
_indices.Clear();
}
public void Insert(int index, T input)
{
if (index == _items.Count)
{
_items.Enqueue(input);
_indices[_items.Count - 1] = input;
return;
}
UpdateInRange(index, 0, _items.Count + 1, () =>
{
_indices.Clear();
var i = 0;
var items = new ConcurrentQueue<T>();
while (_items.TryDequeue(out T item))
{
if (Equals(i, index))
{
items.Enqueue(input);
_indices[items.Count - 1] = input;
}
items.Enqueue(item);
_indices[items.Count - 1] = item;
i++;
}
_items = items;
});
}
public bool Remove(T input)
{
_indices.Clear();
var response = false;
var items = new ConcurrentQueue<T>();
while (_items.TryDequeue(out T item))
{
if (Equals(input, item) is false)
{
response = true;
items.Enqueue(item);
_indices[items.Count - 1] = item;
}
}
_items = items;
return response;
}
public void RemoveAt(int index)
{
UpdateInRange(index, 0, _items.Count, () =>
{
_indices.Clear();
var i = 0;
var items = new ConcurrentQueue<T>();
while (_items.TryDequeue(out T item))
{
if (Equals(i, index) is false)
{
items.Enqueue(item);
_indices[items.Count - 1] = item;
}
i++;
}
_items = items;
});
}
protected IEnumerator<T> Enumerate()
{
using (var enumerator = _items.GetEnumerator())
{
var i = 0;
var items = new ConcurrentQueue<T>();
while (enumerator.MoveNext())
{
var item = _indices.TryGetValue(i++, out T value) ? value : enumerator.Current;
items.Enqueue(item);
yield return item;
}
_items = items;
}
}
protected void UpdateInRange(int index, int min, int max, Action action)
{
if (index < min || index >= max)
{
throw new ArgumentOutOfRangeException("Incorrect index");
}
action();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator() => Enumerate();
IEnumerator IEnumerable.GetEnumerator() => Enumerate();
}
Complete code and tests
Clear()
method, executes_items.Clear();
, at this time another thread callsInsert
, executes_indices[_items.Count - 1] = input;
. Then the first thread calls_indices.Clear();
. \$\endgroup\$_items.ToList().IndexOf(input)
is sucks. Poor performance and excessive memory consumption. \$\endgroup\$ImmutableList
there. \$\endgroup\$