請設計一個線程安全的集合(可以直接繼承自List<T>)

1.對非線程安全類List的一些總結(首先了解)
描述場景:一個項目的一個功能點,需要從接口接受返回數據,並對返回的數據進行一些業務處理,處理完成之後,添加到一個List中,然後在View中循環這個List,展示所有的數據。
每次從接口中取回的數據量不等,最多會有上百條。雖說上百條也不算多,但是每條數據都要經過一系列的業務處理,感覺這樣也挺耗時的,於是考慮使用Parallel.Foreach來進行並行處理。
項目完成之後,對比了一下並行和非並行的情況,發現並行之後並沒有提高多少效能,倒是遇到了一些比較怪異的問題。

出現的問題:Parallel.Foreach 中對List執行Add操作之後,List的Count有時候並不是執行並行的操作的執行次數,而且List中會有Item爲null的情況。

分析:因爲List不是線程安全的類,在多線程情況下就會導致一些不可預知的情況。微軟開放了List源碼,地址:http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,cf7f4095e4de7646
從源碼中我們可以看到List是通過一個Array來進行處理的,如果初始沒有對List設置容量,List容量將爲0,如果此時使用Add添加新項的時候,就會給List設置一個初始容量(初始值爲4)。使用Add添加新項的時候,如果已經達到容量最大值,List會自動擴充容量的值,擴充後的容量的值爲原來既有項目數量的2倍(其實也就是原來容量的2倍)。

我們把Add方法和擴容方法摘抄如下:

public void Add(T item) {
            if (_size == _items.Length) EnsureCapacity(_size + 1);
            _items[_size++] = item;
            _version++;
        }
private void EnsureCapacity(int min) {
            if (_items.Length < min) {
                int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
                // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
                // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
                if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
                if (newCapacity < min) newCapacity = min;
                Capacity = newCapacity;
            }
        }

瞭解了List內部內部擴容情況之後,下面就以上兩個問題進行分析:

1、 List中的Item數量比預期的少。

導致這個問題的原因其實還是挺明顯的。當兩個線程(ThreadA和TreadB),同時調用Add方法添加不同的值的時候,如果此時ThreadA和ThreadB獲取到的size相同,就會出現下面這種情況:

ThreadA:List[size] = A;

ThreadB:List[size] = B;

這種情況下,在size這個位置只會有一個ThreadB設置的值,ThreadA設置的值將會被替換掉,這也就是造成Item數量比預期少的原因。

2、 List中的Item有null。

其實和上面類似,看Add中的代碼:

_items[_size++] = item;
我們改變一下,變成:

(1)_size = _size+1;

(2)Items[_size] = item;

如果ThreadA執行完(1)之後ThreadB獲取到新的_size也執行了(1)那此時_size就相當於是加2了,所以_size+1索引位置的項就是T的默認值了(值類型會值類型的默認值,引用類型爲null)。這樣就能解釋爲什麼會出現null的原因了。

解決方案:其實這兩個問題完全就是同一個問題,只不過表象不同而已。最終解決方案很簡單,要麼自己加鎖,要麼使用線程安全的ConcurrentBag
1.手寫一個類繼承自List,給每個T加上lock鎖,參照代碼如下:

myList.Add(object1);

//修改爲
public static object lockData=new  object();
 
lock(lockData)
{
    myList.Add(object1)
}

2.當排序並不重要時,包可用於存儲對象,而與集不同,包支持重複項。 ConcurrentBag 是一個線程安全包實現,適用於同一線程將生成和使用存儲在包中的數據的情況。
ConcurrentBag 接受 null 作爲引用類型的有效值。
ConcurrentBag使用多種機制來最小化同步的需求。 例如,它爲訪問它的每個線程維護一個本地隊列,並且在某些條件下,線程能夠以無鎖的方式訪問其本地隊列,很少或沒有爭用。 因此,雖然ConcurrentBag有時需要鎖定,但對於某些併發場景(例如,許多以相同速率產生和使用的線程),它是一個非常有效的集合。

https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent.concurrentbag-1?view=netframework-4.7.2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章