對非線程安全類List 的一些總結


一個項目的一個功能點,需要從接口接受返回數據,並對返回的數據進行一些業務處理,處理完成之后,添加到一個List<T>中,然后在View中循環這個List<T>,展示所有的數據。每次從接口中取回的數據量不等,最多會有上百條。雖說上百條也不算多,但是每條數據都要經過一系列的業務處理,感覺這樣也挺耗時的,於是考慮使用Parallel.Foreach來進行並行處理。

項目完成之后,對比了一下並行和非並行的情況,發現並行之后並沒有提高多少效能,倒是遇到了一些比較怪異的問題。

Parallel.Foreach 中對List<T>執行Add操作之后,List<T>的Count有時候並不是執行並行的操作的執行次數,而且List<T>中會有Item為null的情況。其實這個問題的解決方案很簡單,就是因為List<T>不是線程安全的類,在多線程情況下就會導致一些不可預知的情況,加個鎖就可以解決問題了。但是,如果能更好的了解到底是什么原因導致的,豈不更好,於是在一個同事的幫助下,找到了List<T>的源碼(感謝微軟開放了源碼,地址:http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,cf7f4095e4de7646),從List<T>的源碼中更進一步的了解了導致以上問題的原因。

 

在分析上面兩個問題之前,我們先了解一下List<T>的內部情況。從源碼中我們可以看到List<T>是通過一個Array來進行處理的,如果初始沒有對List<T>設置容量,List<T>容量將為0,如果此時使用Add添加新項的時候,就會給List<T>設置一個初始容量(初始值為4)。使用Add添加新項的時候,如果已經達到容量最大值,List<T>會自動擴充容量的值,擴充后的容量的值為原來既有項目數量的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<T>內部內部擴容情況之后,下面就以上兩個問題進行分析。

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

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

ThreadA:List<T>[size] = A;

ThreadB:List<T>[size] = B;

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

2、 List<T>中的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<T>。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM