目录
简介
本章主要介绍并发编程相关的不同的数据结构。包含
- 使用ConcurrentDictionary
- 使用ConcurrentQueue实现异步处理
- 改变ConcurrentStack异步处理顺序
- 使用ConcurrentBag创建一个可扩展的爬虫
- 使用BlockingCollection进行异步处理
ConcurrentQueue集合使用了原子的比较和交换(Compareand Swap,简称CAS)操作,以及SpinWait来保证线程安全。 他实现可一个先进先出的集合,这意味着元素出队列的顺序与加入队列的顺序是一致的。可以调用Enqueue方法向队列中加入元素。TryDequeue方法试图取出队列中的第一个元素,而TryPeek方法则试图得到第一个元素但并不从队列中删除该元素。
ConcurrentStack的实现也没有使用任何锁,只采用了CAS操作。它是一个后进先出( Last In First Out,简称LIFO)的集合,这意味着最近添加的元素会先返回。可以使用Push和PushRange方法添加元素,使用TryPop和TryPopRange方法获取元素,以及使用TryPeek方法检查元素。
ConcurrentBag是一个支持重复元素的无序集合。它针对这样以下情况进行了优化,即多个线程以这样的方式工作:每个线程产生和消费自己的任务,极少与其他线程的任务交互(如果要交互则使用锁)。添加元素使用Add方法,检查元素使用TryPeek方法,获取元素使用 TryTake方法。
请避免使用上面提及的集合的Count属性。实现这些集合使用的是链表,Count操作的时间复杂度为O(N)。如果想检查集合是否为空,请使用IsEmpty属性,其时间复杂度为O(1)。
ConcurrentDictionary是一个线程安全的字典集合的实现。对于读操作无需使用锁。但是对于写操作则需要锁。该并发字典使用多个锁,在字典桶之上实现了一个细粒度的锁模型。使用参数concurrencyLevel可以在构造函数中定义锁的数量,这意味着预估的线程数量将并发地更新该字典。由于并发字典使用锁,所以一些操作需要获取该字典中的所有锁。如果没必要请避免使用以下操作:Count、IsEmpty、Keys、Values、CopyTo及ToArray。
BlockingCollection是对IProducerConsumerCollection泛型接口的实现的一个高级封装。它有很多先进的功能来实现管道场景,即当你有一些步骤需要使用之前步骤运行的结果时。BlockingCollectione类支持如下功能:分块、调整内部集合容量、取消集合操作、从多个块集合中获取元素。
使用ConcurrentDictionary
我们创建两个集合,其中一个标准的字典集合,另一个是并发字典集合。然后采用锁的机制向标准的字典中添加元素,并测量完成100万次迭代的时间。同样也采用同样的场景来测量ConcurrentDictionary的性能,最后比较从两个集合中获取值的性能。
using System.Collections.Concurrent;
const string item = " dictionary item";
const int Iteations = 1000000;
string currentItem;
var concurrentDic = new ConcurrentDictionary<int, string>();
var dic = new Dictionary<int, string>();
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < Iteations; i++)
{
lock (dic)
{
dic[i] = item;
}
}
sw.Stop();
WriteLine($"writing to dictionary with a lock:{sw.Elapsed}");
sw.Restart();
for (int i = 0; i < Iteations; i++)
{
concurrentDic[i] = item;
}
sw.Stop();
WriteLine($"writing to concurrent dictionary :{sw.Elapsed}");
sw.Restart();
for (int i = 0; i < Iteations; i++)
{
lock (dic)
{
currentItem = dic[i];
}
}
sw.Stop();
WriteLine($"reading from dictionary with a lock:{sw.Elapsed}");
sw.Restart();
for (int i = 0; i < Iteations; i++)
{
currentItem = concurrentDic[i];
}
sw.Stop();
WriteLine($"reading from a concurrent dictionary :{sw.Elapsed}");
结果如下,,我们发现ConcurrentDictionary写操作比使用锁的通常的字典要慢得多,而读操作则要快些。因此如果对字典需要大量的线程安全的读操作,ConcurrentDictionary是最好的选择。如果你对字典只需要多线程访问只读元素,则没必要执行线程安全的读操作。在此场景中最好只使用通常的字典或ReadOnlyDictionary集合。
使用ConcurrentQueue实现异步处理
本节将展示创建能被多个工作者异步处理的一组任务例子。 使用 当程序运行时,我们使用ConcurrentQueue集合实例创建了一个任务队列。然后创建了一个取消标志,它是用来在我们将任务放入队列后停止工作的。接下来启动了一个单独的工作者线程来将任务放入任务队列中。该部分为异步处理产生了工作量。现在定义该程序中消费任务的部分。我们创建了四个工作者,它们会随机等待一段时间,然后从任务队列中获取一个任务,处理该任务,一直重复整个过程直到我们发出取消标志信号。最后,我们启动产生任务的线程,等待该线程完成。然后使用取消标志给消费者发信号我们完成了工作。最后一步将等待所有的消费者完成。
using System.Collections.Concurrent;
var t = RunProgram();
await t;
WriteLine("运行结束");
ReadLine();
async Task RunProgram()
{
var taskQueue = new ConcurrentQueue<CustomTask>();
var cts = new CancellationTokenSource();
var taskSource = Task.Run(() => TaskProducer(taskQueue));
Task[] processors = new Task[4];
for (int i = 1; i <= 4; i++)
{
string processorId = i.ToString();
processors[i - 1] = Task.Run(() => TaskProcessor(taskQueue, $"Processor {processorId}", cts.Token));
}
await taskSource;
cts.CancelAfter(TimeSpan.FromSeconds(2));
await Task.WhenAll(processors);
}
async Task TaskProducer(ConcurrentQueue<CustomTask> queue)
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(50);
var workItem=new CustomTask { Id = i };
queue.Enqueue(workItem);
WriteLine($"Task {workItem.Id} has been posted");
}
}
async Task TaskProcessor(ConcurrentQueue<CustomTask> queue,string name,CancellationToken token)
{
CustomTask workItem;
bool dequeueSucesful = false;
await GetRandomDelay();
do
{
dequeueSucesful=queue.TryDequeue(out workItem);
if (dequeueSucesful)
{
WriteLine($"Task {workItem.Id} has been prcessed by name");
}
await GetRandomDelay();
}
while(!token.IsCancellationRequested);
}
Task GetRandomDelay()
{
int delay = new Random(DateTime.Now.Millisecond).Next(1, 500);
return Task.Delay(delay);
}
class CustomTask
{
public int Id { get; set; }
}
结果如下图,我们看到队列中的任务按从前到后的顺序被处理,但一个后面的任务是有可能会比前面的任务先处理的,因为我们有四个工作者独立地运行,而且任务处理时间并不是恒定的。我们看到访问该队列是线程安全的,没有一个元素会被提取两次。