C# - 高效的线程安全队列 ConsurrentQueue<T>
常用方法
入队 | EnQueue | void |
出队(获取第一个元素) | TryPeek | bool |
出队 | TryDequeue | bool |
是否为空 | IsEmpty | bool |
获取队列内元素数量 | Count | int |
入队操作
如上图所示,入队操作是在尾部的段中进行,当数据进入段内失败时会先进行一个回退操作然后再不断尝试直到成功,这里失败的原因(tail.Append(item)返回false)只有一个就是当该段内的空间不够时正在分配新的段,这段时间内会进入该段的元素会失败。
出队操作
如上图所示,出队失败时返回false 而不是像入队一样进行回退操作,因为出队失败的原因只有一个就是当队列内所有段的元素为空时,所以出队设计成了返回bool值的函数。
判断是否为空(IsEmpty)
整个判断为O(1)的复杂度 主要有三种情况:
1. 头节点(段)不为空返回false
2. 头节点为空而且下一个节点也为空返回true
3. 头节点为空而且下一个节点不为空返回false,这种情况说明队列正在扩容,所以要自选等待扩容完毕时再次进行判断
获取队列内元素数量(Count)
找到头节点的low的位置和尾节点的high的位置,由于每个段内记录了当前段在队列中的索引,所以很容易求出整个队列中元素的数量。
跟ConcurrentStack一样 微软官方文档和注释中也说明:判断队列是否为空要使用IsEmpty属性而不是判断Count == 0 原因在于GetHeadTailPositions在大量数据入队和出队的过程中寻找头尾节点的位置是比较耗时的操作,要不断循环确定头尾节点的位置,所以判断队列是否为空还是使用IsEmpty属性。
案例
using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; namespace ConcurrentQueueStudy { class Program { static void Main(string[] args) { ConcurrentQueue<int?> queue = new ConcurrentQueue<int?>(); for (int i = 0; i < 5; i++) { Task.Run(() => { int i = 0; while (true) { i++; queue.Enqueue(i); Thread.Sleep(10); } }); } for (int i = 0; i < 10; i++) { Task.Run(() => { while (true) { if (!queue.IsEmpty) { int? num; bool result = queue.TryDequeue(out num);//多线程的情况下偶尔会出现空值的情况 if(result) Console.WriteLine(num);//在这里判断是否有返回的值,防止上面多线程出现问题 } //else Console.WriteLine("无值"); Thread.Sleep(10); } }); Thread.Sleep(10); } while (true) { Thread.Sleep(1000); Console.WriteLine($"队列数:{queue.Count}"); } } } }