Yan Cui
I help clients go faster for less using serverless technologies.
This article is brought to you by
MongoDB 8.0 is here to change the game. Faster reads and inserts, and brand-new vector search to support modern AI-powered apps.
I had a problem with the project I’m working on at work where on the base class I had a list which its child classes need to have access to in a read-only capacity but manipulation to the list itself is handled by the base class only. However, the standard C# List<T> and Enumerator<T> are not thread-safe and we started seeing problems when the list is modified by one thread whilst another is trying to loop through it.
Whilst looking for a clean solution we found this article on CodeProject:
http://www.codeproject.com/KB/cs/safe_enumerable.aspx
The article had covered much of the implementation you need, but left some gaps you need to plug yourself for a thread-safe list which I have included below along with some of the useful methods you’d find on List<T>:
Thread-safe Enumerator<T>
/// <summary> /// A thread-safe IEnumerator implementation. /// See: http://www.codeproject.com/KB/cs/safe_enumerable.aspx /// </summary> public class SafeEnumerator<T>: IEnumerator<T> { // this is the (thread-unsafe) // enumerator of the underlying collection private readonly IEnumerator<T> _inner; // this is the object we shall lock on. private readonly object _lock; public SafeEnumerator(IEnumerator<T> inner, object @lock) { _inner = inner; _lock = @lock; // entering lock in constructor Monitor.Enter(_lock); } public T Current { get { return _inner.Current; } } object IEnumerator.Current { get { return Current; } } public void Dispose() { // .. and exiting lock on Dispose() // This will be called when foreach loop finishes Monitor.Exit(_lock); } /// <remarks> /// we just delegate actual implementation /// to the inner enumerator, that actually iterates /// over some collection /// </remarks> public bool MoveNext() { return _inner.MoveNext(); } public void Reset() { _inner.Reset(); } }
Thread-safe List<T>
/// <summary> /// A thread-safe IList implementation using the custom SafeEnumerator class /// See: http://www.codeproject.com/KB/cs/safe_enumerable.aspx /// </summary> public class SafeList<T> : IList<T> { // the (thread-unsafe) collection that actually stores everything private readonly List<T> _inner; // this is the object we shall lock on. private readonly object _lock = new object(); public SafeList() { _inner = new List<T>(); } public int Count { get { lock (_lock) { return _inner.Count; } } } public bool IsReadOnly { get { return false; } } public T this[int index] { get { lock (_lock) { return _inner[index]; } } set { lock (_lock) { _inner[index] = value; } } } IEnumerator<T> IEnumerable<T>.GetEnumerator() { lock (_lock) { // instead of returning an usafe enumerator, // we wrap it into our thread-safe class return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock); } } /// <remarks> /// To be actually thread-safe, our collection must be locked on all other operations /// </remarks> public void Add(T item) { lock (_lock) { _inner.Add(item); } } public void Clear() { lock (_lock) { _inner.Clear(); } } public bool Contains(T item) { lock (_lock) { return _inner.Contains(item); } } public void CopyTo(T[] array, int arrayIndex) { lock (_lock) { _inner.CopyTo(array, arrayIndex); } } public bool Remove(T item) { lock (_lock) { return _inner.Remove(item); } } public IEnumerator GetEnumerator() { lock (_lock) { return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock); } } public int IndexOf(T item) { lock (_lock) { return _inner.IndexOf(item); } } public void Insert(int index, T item) { lock (_lock) { _inner.Insert(index, item); } } public void RemoveAt(int index) { lock (_lock) { _inner.RemoveAt(index); } } public ReadOnlyCollection<T> AsReadOnly() { lock (_lock) { return new ReadOnlyCollectio<T>(this); } } [CheckParameters] public void ForEach([NotNull] Action<T> action) { lock (_lock) { foreach (var item in _inner) { action(item); } } } [CheckParameters] public bool Exists([NotNull] Predicate<T> match) { lock (_lock) { foreach (var item in _inner) { if (match(item)) { return true; } } } return false; } }
Thread-safe IEnumerable<T>
/// <summary> /// A thread-safe IEnumerable implementation /// See: http://www.codeproject.com/KB/cs/safe_enumerable.aspx /// </summary> public class SafeEnumerable<T> : IEnumerable<T> { private readonly IEnumerable<T> _inner; private readonly object _lock; public SafeEnumerable(IEnumerable<T> inner, object @lock) { _lock = @lock; _inner = inner; } public IEnumerator<T> GetEnumerator() { return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
Whenever you’re ready, here are 3 ways I can help you:
- Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
- I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
- Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.
This is one of the best answer so far, I have read online. Just useful information. Very well presented. Thanks for sharing with us. I had found another nice post with wonderful explanation on Enumeration in c#, which is also helped me to complete my task. For more details of that post check out this link….
http://mindstick.com/Articles/ade257fc-7058-4f60-a0fe-85c7ca52f004/?Enumeration%20in%20c#
Thanks everyone for your precious post.
Just want to point out that your implementation is flawed, try this:
foreach(int value in safeList)
{
safeList.Remove(value);
}
Just would like to share with others that nowadays this approach is not a good idea. You might not even notice the problem in the beginning but if you use async/await inside the foreach enumeration Monitor.Exit might be called on a different from the Monitor.Enter thread which leads to an unpredictable exception.