Yan Cui
I help clients go faster for less using serverless technologies.
This article is brought to you by
Don’t reinvent the patterns. Catalyst gives you consistent APIs for messaging, data, and workflow with key microservice patterns like circuit-breakers and retries for free.
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.