1. 程式人生 > >對非執行緒安全類List的一些總結

對非執行緒安全類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>。