请设计一个线程安全的集合(可以直接继承自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

posted @ 2019-11-27 21:22  CH|heyi  阅读(397)  评论(0编辑  收藏  举报