最近在做windows runtime下APP開發的工作。在Service層請求返回后,往往會通過回調的形式來通知UI更新數據。多個線程操作經常出現foreach拋出異常:System.InvalidOperationException: 集合已修改;可能無法執行枚舉操作,導致APP crash。
在網上搜索了一下,得出以下結論:
- 實現一個真正線程安全的List是很困難的,具體可以參考這篇Why are thread safe collections so hard?。
- 使用ConcurrentBag<T>,微軟給出的線程安全的集合,缺點是unordered。如果集合依賴內部元素的順序,就不太合適了。
- 實現一個枚舉安全的List,所需的工作量相對小很多,甚至僅需要給已用到的List操作加上lock。
以下是一個最小化實現的枚舉安全的List。因為實際工程中,需要枚舉安全的集合僅用到了Add,Count,索引等操作,所以繼承了IEnumerable接口,而不是IList。同時也不影響使用Linq to objects的擴展方法,真是偷了一個大懶。
class EnumerationSafeList<T> : IEnumerable<T> { private List<T> innerList = new List<T>(); private object lockObject = new object(); public IEnumerator<T> GetEnumerator() { return Clone().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return Clone().GetEnumerator(); } public void Add(T item) { lock(lockObject) { innerList.Add(item); } } public void Remove(T item) { lock(lockObject) { innerList.Remove(item); } } public int Count { get { lock(lockObject) { return innerList.Count; } } } public T this[int index] { get { lock(lockObject) { return innerList[index]; } } set { lock(lockObject) { innerList[index] = value; } } } private List<T> Clone() { var cloneList = new List<T>(); lock(lockObject) { foreach (var item in innerList) { cloneList.Add(item); } } return cloneList; } }
代碼對Add,Remove,Count和索引四個操作加了lock,同時在枚舉時通過加lock並返回當前集合的副本,來避免遍歷時因為其他線程的修改而拋出異常。
如果代碼需要List類型的全部方法,就需要進一步修改,把IEnumerable改成IList並實現接口,就可以得到一個完整的“data thread safe list”。
完整的代碼及測試用的程序:代碼