1
\$\begingroup\$

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

https://github.com/artemiusgreat/Concurrent-List

\$\endgroup\$
15
  • 2
    \$\begingroup\$ Your implementation is not thread-safe. One thread calls the Clear() method, executes _items.Clear();, at this time another thread calls Insert, executes _indices[_items.Count - 1] = input;. Then the first thread calls _indices.Clear();. \$\endgroup\$ May 19, 2022 at 11:42
  • 2
    \$\begingroup\$ _items.ToList().IndexOf(input) is sucks. Poor performance and excessive memory consumption. \$\endgroup\$ May 19, 2022 at 11:44
  • 2
    \$\begingroup\$ Please check this SO old but gold topic. \$\endgroup\$ May 19, 2022 at 12:42
  • 2
    \$\begingroup\$ Look at Immutable collections. There is an ImmutableList there. \$\endgroup\$ May 19, 2022 at 14:47
  • 1
    \$\begingroup\$ Then how about using a Synchronized SortedList? \$\endgroup\$ May 20, 2022 at 7:44

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.