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<T> list) : this(list, new 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<T> list, object 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[] array, int arrayIndex)
{
lock (_lockObject)
_list.CopyTo(array, arrayIndex);
}
/// <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<T> GetEnumerator()
{
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 index, T item)
{
lock (_lockObject)
_list.Insert(index, item);
}
/// <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 { get; set; }
public List<int> NotSoSimpleValue { 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<int> NotSoSimpleValue
{
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<int> NotSoSimpleValue { 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];
});