集合类的需求总是源源不断,因此,不管是1.0到2.0的泛型,还是3.0到4.0的并行(本文的并行指Concurrent,非 Parallel),.NET每个版本总会伴随着一些集合类的增长。由于并行计算现在已经越来越流行,这里我将对.NET 4.0中新增的命名空间System.Collections.Concurrent和它下面的类做一些简单的介绍。
为什么需要Concurrent?
相信不少朋友都有多线程编程的经历吧,不过在.NET 4.0以前,多线程编程下很容易出问题,先看个简单的例子吧。
static void main()
{
myList = new List<string>();
for (int i = 0; i < 1000; i++)
{
myList.Add(i.ToString());
}
new Thread(T2).Start();
new Thread(T3).Start();
}
static IList<string> myList;
static void T2()
{
Thread.Sleep(100);
for (int i = 0; i < 50; i++)
{
myList.Remove(i.ToString());
}
}
static void T3()
{
foreach (var a in myList)
{
Console.WriteLine(a);
}
}
在这个例子中,我们首先初始化一个长度为1000数组,然后我们开启两个线程,一个进行删除操作,另外个则进行简单的读操作。运行代码的话你会发现程序会抛出InvalidOperationException,因为系统集合在被读的同时被修改了,因此列举操作可能不能执行。
当然在4.0以前我们也有办法避免这种类似的操作,比如我们可以对要操作的对象加锁。即我们可以在对myList集合进行读写操作之前对其添加代码lock (myList)。
然而这种方式毕竟不够简洁,并且在更复杂的情况下它可能会显得非常繁琐。这时候支持并行操作的集合应运而生了。
有哪些Concurrent集合?
在System.Collections.Concurrent公开的类并不多,他们分别在两个不同的dll中存在,其中在System.dll中仅仅一个BlockingCollection<T>和ConcurrentBag<T>,在mscorlib.dll中则稍多一些,他们分别是ConcurrentQueue<T>, ConcurrentStack<T>, ConcurrentDictionary<TKey, TValue>。不过对于日常开发来说,他们基本够用。下面我们来一个个看下这些类的构造吧。
首先看看mscorlib中的几个类,这些类其实我们在.NET 2.0中已经接触过它们的普通版本,因此它们功能基本不变,因此你仍然可以像以前那样使用普通版本的哈希表,队列和堆栈来使用它们。
不过并行类不仅仅简单以前的集合类改造成线程安全的并行类,它同时还提供了一些更丰富的功能,由于Lamda表达式的引入,现在在 ConcurrentDictionary<TKey, TValue>你可以通过AddOrUpdate或GetOrAdd添加自己的值生成方案。这使得我们在生成键值对的时候更加方便和简单了。如:
ConcurrentDictionary<int, string> td = new ConcurrentDictionary<int, string>();
Func<int, string> genVar = (i) => i.ToString();
Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000; i++)
{
td.GetOrAdd(i, genVar);
}
});
Task.Factory.StartNew(() =>
{
Func<int, string, string> updateVar = (key, oldVar) => oldVar + key;
td.AddOrUpdate(0, genVar, updateVar);
Console.WriteLine(td[0]);
}).Wait();
我们可以看到,现在我们在取值的时候,如果哈希表中没有该值的话我们可以自己生成,或者我们在添加新的值的时候如果碰到重复键值的时候也可以很方便的解决掉了。相对以前在添加或者查找数据时痛苦的判断,这种方式更加简洁和方便。
其他几个类相对来说函数使用变化不大,唯一区别比较大的也是在Concurrent命名空间中很常见的各种try函数操作。
接着我们来看看在System.dll中的并行集合,这里面的两个类在以前的.NET中是没有的。首先看看 ConcurrentBag<T>,顾名思义,这个类提供并行数据包的功能,这个类相对来说构造比较简单,它继承自四个接口:IProducerConsumerCollection<T>, IEnumerable<T>, ICollection, IEnumerable,后面三个集合类常用的接口我们当然很熟悉,而且由于实现了IEnumerable<T>接口,ConcurrentBag<T>也支持LINQ操作。不过这里有个特殊的接口 IProducerConsumerCollection<T>我们以前没有见过,虽然是个新的接口,但故名思意,这个接口提供了生产者/消费者的集合操作,它提供了四个基本的方法:CopyTo(T[],int), ToArray(), TryAdd(T), TryTake(out T)。其中我们主要关注后面的两个方法,TryAdd是添加元素操作,而TryTake则是取元素操作。不过在ConcurrentBag 中,TryAdd方法被设置为protected,外部对象需要通过Add操作来添加元素。另外,取元素的话,除了TryTake之外,我们还可以通过 TryPeek来取集合当前的最后一个元素而不删除它。下面来看看例子:
static void main()
{
ConcurrentBag<string> bag = new ConcurrentBag<string>();
Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000; i++)
{
bag.Add(i.ToString());
}
bag.Add("Last");
});
Task.Factory.StartNew(() =>
{
foreach (string item in bag)
{
Console.WriteLine(item);
}
}).Wait();
}
BlockingCollection<T>相对来说,要稍微复杂一些,它实现了四个接口:IEnumerable<T>, ICollection, IEnumerable, IDisposable,因此该集合同样支持LINQ,不仅如此,它也提供了比ConcurrentBag更加丰富的功能。如 CompleteAdding和超时设置的TryAdd和TryTake方法等等。
总结
随着多线程和并行编程的要求越来越多,相信在未来,.NET家族新增的这些类将会在我们的日常编程生活中越来越常见,所以掌握它们也显得越来越又必要了。