Tidbits

Wrapper for locking IList

Something simple and quick. Might not fit your needs, so check that first.

/// <summary>
/// A wrapper for <see cref="IList{T}"/> that blocks simultaneous access from separate threads.
/// </summary>
/// <typeparam name="T"></typeparam>
public class LockedListWrapper<T> : IList<T>
{
    private readonly IList<T_list;
    private readonly object _lockObject;
 
    /// <summary>
    /// Creates a new <see cref="LockedListWrapper{T}"/> with a private lock
    /// </summary>
    /// <param name="list">The list to wrap around</param>
    public LockedListWrapper(IList<Tlist) : this(listnew object())
    {
    }
 
    /// <summary>
    /// Creates a new <see cref="LockedListWrapper{T}"/> using a specific object to lock access
    /// </summary>
    /// <param name="list">The list to wrap around</param>
    /// <param name="lockObject">The object to lock access with</param>
    public LockedListWrapper(IList<Tlistobject lockObject)
    {
        _list = list ?? throw new ArgumentNullException(nameof(list));
        _lockObject = lockObject ?? throw new ArgumentNullException(nameof(lockObject));
    }
 
    /// <inheritdoc />
    public void Add(T item)
    {
        lock (_lockObject)
            _list.Add(item);
    }
 
    /// <inheritdoc />
    public void Clear()
    {
        lock (_lockObject)
            _list.Clear();
    }
 
    /// <inheritdoc />
    public bool Contains(T item)
    {
        lock (_lockObject)
            return _list.Contains(item);
    }
 
    /// <inheritdoc />
    public void CopyTo(T[] arrayint arrayIndex)
    {
        lock (_lockObject)
            _list.CopyTo(arrayarrayIndex);
    }
 
    /// <inheritdoc />
    public bool Remove(T item)
    {
        lock (_lockObject)
            return _list.Remove(item);
    }
 
    /// <inheritdoc />
    public int Count
    {
        get
        {
            lock (_lockObject)
                return _list.Count;
        }
    }
 
    /// <inheritdoc />
    public bool IsReadOnly
    {
        get
        {
            lock (_lockObject)
                return _list.IsReadOnly;
        }
    }
 
    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    /// <inheritdoc />
    public IEnumerator<TGetEnumerator()
    {
        lock (_lockObject)
            return new List<T>(_list).GetEnumerator();
    }
 
    /// <inheritdoc />
    public int IndexOf(T item)
    {
        lock (_lockObject)
            return _list.IndexOf(item);
    }
 
    /// <inheritdoc />
    public void Insert(int indexT item)
    {
        lock (_lockObject)
            _list.Insert(indexitem);
    }
 
    /// <inheritdoc />
    public void RemoveAt(int index)
    {
        lock (_lockObject)
            _list.RemoveAt(index);
    }
 
    /// <inheritdoc />
    public T this[int index]
    {
        get
        {
            lock (_lockObject)
                return _list[index];
        }
        set
        {
            lock (_lockObject)
                _list[index] = value;
        }
    }
}

What is this good for though?


Imagine a class like this:

public class Example
{
    public int SimpleValue { getset; }
 
    public List<intNotSoSimpleValue { get; }
 
    public Example()
    {
        NotSoSimpleValue = new List<int>();
    }
}

What if you need to share this between threads? You might get away with the integer, but a complex object like List will run into issues at some point.

So you try to add locks:

public class Example
{
    private readonly object _lock;
    private readonly List<int_notSoSimpleValue;
    private int _simpleValue;
 
    public int SimpleValue
    {
        get
        {
            lock (_lock)
            {
                return _simpleValue;
            }
        }
        set
        {
            lock (_lock)
            {
                _simpleValue = value;
            }
        }
    }
 
    public List<intNotSoSimpleValue
    {
        get
        {
            lock (_lock)
            {
                return _notSoSimpleValue;
            }
        }
    }
 
    public Example()
    {
        _lock = new object();
        _notSoSimpleValue = new List<int>();
    }
}

And sure, you don’t have to worry about the integer being half written while another thread reads it.
But List is only a reference to the value. The lock prevents two threads from getting that reference at the same time, but not from interacting with what it references.

The wrapper above now allows us to add locks to that reference as well:

public class Example
{
    private readonly object _lock;
    private readonly List<int_notSoSimpleValue;
    private int _simpleValue;
 
    public int SimpleValue
    {
        get
        {
            lock (_lock)
            {
                return _simpleValue;
            }
        }
        set
        {
            lock (_lock)
            {
                _simpleValue = value;
            }
        }
    }
 
    public LockedListWrapper<intNotSoSimpleValue { get; }
 
    public Example()
    {
        _lock = new object();
        _notSoSimpleValue = new List<int>();
        NotSoSimpleValue = new LockedListWrapper<int>(_notSoSimpleValue);
    }
}

Next, since we have a reference to the original List, and the object used for the lock can be passed in the constructor, we can sync operations in the list with the original objects locking mechanism:

public Example()
{
    _lock = new object();
    _notSoSimpleValue = new List<int>();
    NotSoSimpleValue = new LockedListWrapper<int>(_notSoSimpleValue_lock);
}
 
public int Sum()
{
    int sum = 0;
 
    lock (_lock)
    {
        for (int i = 0; i < _notSoSimpleValue.Count; ++i)
            sum += _notSoSimpleValue[i];
    }
 
    return sum;
}

If we didn’t add all the values inside the list within a single lock block, we might mix two different states of the list. Not very useful.

Lastly, GetEnumerator. While most operations in IList can be performed quickly without much worry, the enumerator is giving us the same problem we had originally: Returning a reference to something we have no direct control over.
And even if we could prevent access to that in some way, we would effectively block any changes to the list while someone uses the enumerator.

To prevent that the class I shared with you copies the list into a buffer, which is then used to iterate. It forces iterations to happen in a snapshot of the list, rather than the original.
This has of course the downside of extra memory consumption, including the overhead for copying the values over.

As an alternative you could implement a custom enumerator that allows for changes to the list in between reading individual indices.


A little addition to the original class that can be misused to block access permanently, but is rather useful sometimes:

/// <summary>
/// Acquires and keeps a lock for the duration of an action
/// </summary>
/// <param name="action">An action to perform with exclusive access to the list</param>
public void RunLocked(Action<LockedListWrapper<T>> action)
{
    if (action is null)
        return;
 
    lock (_lockObject)
        action?.Invoke(new LockedListWrapper<T>(_list));
}

This method allows outside code to join multiple operations on the list inside a single lock, without needing direct access to the internal lock or list.

NotSoSimpleValue.RunLocked(l =>
{
    for (int i = 0; i < l.Count; ++i)
        ++l[i];
});
Explained

List

The following references the implementation of System.Collections.Generic.List<T> in the .NET Framework 4.8, as it is shown here: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,cf7f4095e4de7646
The code below is simplified to show the ideas. No verification, exception handling, optimizations, etc. In other words: unsafe.
Please do not use in your project as is.


What problem is List trying to solve?
Basically, the fixed size of arrays.
To do so, List uses an array to store data and offers convenient methods to copy it into a larger arrays, should the old one not have enough space anymore. Simple, right?
Right.
No, really. There’s no catch here. It really is that simple.

However, that doesn’t mean you can’t benefit from knowing some of the implementation details!
So let’s create our own version of List and try to see how the “official” version handles different problems.


We’re going to start with a naive approach:

public class List<T>
{
    private T[] _container;
 
    public T this[int index]
    {
        get => _container[index];
        set => _container[index] = value;
    }
 
    public int Size => _container.Length;
 
    public List()
    {
        _container = new T[0];
    }
 
    public void Add(T item)
    {
        T[] newContainer = new T[_container.Length + 1];
 
        Array.Copy(_container, newContainer, _container.Length);
 
        newContainer[_container.Length] = item;
 
        _container = newContainer;
    }
}

Technically it works, but allocating space for a new array takes time.
Since we’re probably going to add quite a few items over the lifetime of this List, we should always allocate a bit more space than needed, so we have a buffer that can fill up instead without bothering the memory so much.

This is the first detail you might be interested in:
How much extra space do we allocate?
If we take too much, we’re wasting memory.
If we take too little, we might need to allocate space again, wasting time (and for a brief time occupy all the old and new memory).

In a specific use case you might be able to predict the usage of your List, in which case you can benefit from your own implementation.
For a general purpose library however, there is no ideal answer.

The Framework has a rather straightforward solution:
Need more space? Double what you have!

public class List<T>
{
    private T[] _container;
 
    private int _usedSpace;
 
    public T this[int index]
    {
        get => _container[index];
        set => _container[index] = value;
    }
 
    public int Size => _usedSpace;
 
    public List()
    {
        _container = new T[0];
        _usedSpace = 0;
    }
 
    public void Add(T item)
    {
        if (_usedSpace < _container.Length)
        {
            _container[_usedSpace++] = item;
            return;
        }
 
        T[] newContainer = new T[_usedSpace < 1 ? 1 : _usedSpace * 2];
 
        Array.Copy(_container, newContainer, _container.Length);
 
        newContainer[_usedSpace++] = item;
 
        _container = newContainer;
    }
}

That’s it. A new field is needed to keep track of how much of the array is actually used, and at which index the buffer starts.
Don’t forget our List might contain 0 items. Add a minimum initial size for adding items!
(The Framework uses a default of 4 items, by the way)

On a different note, did you notice how we move the data between arrays?
The Framework uses the same approach. Pretty much every operation is using Array.Copy.


One more thing that doesn’t come up often:
How do we handle adding/removing several items at once?
The official List has two (and a half) methods that accept IEnumerable. What happens to those?

The important part first: The default approach is to enumerate through all items and process each as it’s own thing. This way isn’t exactly efficient, but in some cases there is no other way (when you can’t enumerate again and don’t know how many additions you’re going to make).

However, if your IEnumerable is actually a type of ICollection, things get easier. Now we have access to Count, which means we can plan ahead. At most we need to create a bigger array once and can use a single Array.Copy to transfer the data.

public void Add(T item)
{
    IncreaseSize(1);
 
    _container[_usedSpace++] = item;
}
 
public void Add(IEnumerable<Titems)
{
    if (items is ICollection<T> collection)
    {
        if (collection.Count == 0)
            return;
 
        IncreaseSize(collection.Count);
 
        collection.CopyTo(_container_usedSpace);
 
        _usedSpace += collection.Count;
    }
    else
    {
        foreach (T item in items)
            Add(item);
    }
}
 
private void IncreaseSize(int additionalSpaceNeeded)
{
    if (_usedSpace + additionalSpaceNeeded <= _container.Length)
        return;
 
    int newSize = _container.Length * 2;
 
    if (_usedSpace + additionalSpaceNeeded > newSize)
        newSize = _usedSpace + additionalSpaceNeeded;
 
    T[] newContainer = new T[newSize];
 
    Array.Copy(_container, newContainer, _container.Length);
 
    _container = newContainer;
}

As you can see, we’ve added an overload for Add that accepts an IEnumerable, as well as a dedicated method for increasing our container space (to avoid duplicate code).


Inserting items at specific positions or removing them is basically just moving the values inside the container back and forth using Array.Copy, nothing fancy to see here.

And with that you already have a simple List.
The official version has of course more than this.
That means on one side more care for special cases, like the size growing beyond the limit of an int. On the other it includes fancy methods for searching and sorting – which you might find are being delegated to Array, since under the hood you’re using one anyway.


Conclusion:
The List implementation is rather straightforward. No big surprises or mind benders.
Personally, what i took away from it are the growth rate, and how it can pay off to use the TrimExcess method to keep it in check, as well as a renewed confidence in AddRange.