前言
在C#语言中当需要处理并发的场景时,就需要程序员使用最合理的数据结构。那么哪些数据结构是支持和可以在并行计算中被使用的呢。
首先这些数据结构具备可伸缩性,尽可能地避免锁(会造成多个线程的等待,防止资源竞争),同时还能提供线程安全的访问。
在.NET Framework4.0中引入了System.Collections.Concurrent命名空间,其中就包含几个数据结构。
-
ConcurrentQueue
-
ConcurrentDictionary
-
ConcurrentStack
-
ConcurrentBag
-
BlockingCollect
那么接下来,我们来看看这些支持并行计算的数据结构到底应该如何使用!
ConcurrentQueue
该集合使用了原子的比较和交换(CAS),以及SpinWait来保持线程安全。
它实现了一个先进先出(First In First Out简称FIFO)的集合。元素的出队列和加入队列的顺序是一致的。
Enqueue(): 向队列中加入元素。
TryDequeue(): 取出队列中的第一个元素,此时队里无此元素。
TryPeek(): 得到第一个元素但并不从队列中删除该元素。
ConcurrentStack
此集合在实现层也没有加入任何锁,只采用了CAS操作。
它是一个后进后出(Last In First Out,简称LIFO)集合,最近添加的元素先出去(类比为栈)。
Push()和PushRange(): 给该集合添加元素。
TryPop()和TryPopRange(): 从该集合获取元素。
TryPeek(): 检查元素。
ConcurrentBag
该集合是一个支持重复元素的无序集合,专门针对下面多个线程工作时,集合进行了优化。每个线程产生和消费自己的任务,极少与其他线程的任何交互(若需要使用交互,则必须使用锁操作)。
Add(): 添加元素。
TryPeek(): 检查元素。
TryTake(): 获取元素。
ConcurrentDictionary
是一个线程安全的字典集合。其中读操作无需使用锁,写操作需要使用锁。
该并发字典使用多个锁,在字典之上实现一个细粒度的锁模型。其中使用concurrencyLevel可以在构造函数中定义锁的数量,那么说意味着预估的线程数量将并发地更新该字典。
由于并发字典使用锁,所以一些操作需要获取该字典中的所有锁。若是在编程过程中,没有必要则不要调用下面方法:Count,IsEmpty,Keys,Values,CopyTo及ToArray。
BlockingCollection
该集合是对泛型接口IProducerConsumerCollection实现的一个高级封装。其中有很多管道场景,即当你有一些操作需要使用之前计算的结果。
BlockingCollection支持如下功能:
-
分块
-
调整内部集合容量
-
取消集合操作
-
从多个块集合中获取元素
Demo
在单线程的环境中使用通用字典与使用通用字典的性能。
使用ConcurrentDictionary
class Program
{
const string Item = "";
public static string CurrentItem;
static void Main(string[] args)
{
var concurrentDicrionary=new ConcurrentDictionary<int ,string>();
var dictionary=new Dictionary<int ,string>();
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
lock(dictionary)
{
dictionary[i]=Item;
}
}
sw.Stop();
Console.WriteLine("写dictionary的时间"+sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
concurrentDicrionary[i] = Item;
}
sw.Stop();
Console.WriteLine("写并发集合concurrentDicrionary的时间:" + sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
lock(dictionary)
{
CurrentItem = dictionary[i];
}
}
sw.Stop();
Console.WriteLine("读dictionary的时间" + sw.Elapsed);
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
CurrentItem=concurrentDicrionary[i];
}
sw.Stop();
Console.WriteLine("读并发集合concurrentDicrionary的时间:" + sw.Elapsed);
Console.ReadKey();
}
}
可以发现使用ConcurrentDictionary写操作比使用锁的通用字典要慢很多,而读操作则更快些。因此如果对字典需要大量的线程安全的读操作,则ConcurrentDictionary是更好的选择。
小寄语
人生短暂,我不想去追求自己看不见的,我只想抓住我能看得见的。