请设计一个线程安全的集合(可以直接继承自List<T>)
1.对非线程安全类List的一些总结(首先了解)
描述场景:一个项目的一个功能点,需要从接口接受返回数据,并对返回的数据进行一些业务处理,处理完成之后,添加到一个List
每次从接口中取回的数据量不等,最多会有上百条。虽说上百条也不算多,但是每条数据都要经过一系列的业务处理,感觉这样也挺耗时的,于是考虑使用Parallel.Foreach来进行并行处理。
项目完成之后,对比了一下并行和非并行的情况,发现并行之后并没有提高多少效能,倒是遇到了一些比较怪异的问题。
出现的问题:Parallel.Foreach 中对List
分析:因为List
从源码中我们可以看到List
我们把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
导致这个问题的原因其实还是挺明显的。当两个线程(ThreadA和TreadB),同时调用Add方法添加不同的值的时候,如果此时ThreadA和ThreadB获取到的size相同,就会出现下面这种情况:
ThreadA:List
ThreadB:List
这种情况下,在size这个位置只会有一个ThreadB设置的值,ThreadA设置的值将会被替换掉,这也就是造成Item数量比预期少的原因。
2、 List
其实和上面类似,看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
myList.Add(object1);
//修改为
public static object lockData=new object();
lock(lockData)
{
myList.Add(object1)
}
2.当排序并不重要时,包可用于存储对象,而与集不同,包支持重复项。 ConcurrentBag
ConcurrentBag
ConcurrentBag