一. 四大并发集合类
背景:我们目前使用的所有集合都是线程不安全的 。
A. ConcurrentBag:就是利用线程槽来分摊 Bag 中的所有数据,链表的头插法,0 代表移除最后一个插入的值.
(等价于同步中的 List)
B. ConcurrentStack:线程安全的 Stack 是使用 Interlocked 来实现线程安全, 而没有使用内核锁.
(等价于同步中的数组)
C. ConcurrentQueue: 队列的模式,先进先出
(等价于同步中的队列)
D. ConcurrentDictionary: 字典的模式
(等价于同步中的字典)
以上四种安全的并发集合类,也可以采用同步版本+Lock 锁(或其它锁)来实现
代码实践:
01-ConcurrentBag { Console.WriteLine("---------------- 01-ConcurrentBag ---------------------"); ConcurrentBag<int> bag = new ConcurrentBag<int>(); bag.Add(1); bag.Add(2); bag.Add(33); //链表的头插法,0代表移除最后一个插入的值 var result = 0; //flag为true,表示移除成功,并且返回被移除的值 var flag = bag.TryTake(out result); Console.WriteLine("移除的值为:{0}", result);
} #endregion
02-ConcurrentStack { Console.WriteLine("---------------- 02-ConcurrentStack ---------------------"); ConcurrentStack<int> stack = new ConcurrentStack<int>(); stack.Push(1); stack.Push(2); stack.Push(33); //链表的头插法,0代表移除最后一个插入的值 var result = 0; //flag为true,表示移除成功,并且返回被移除的值 var flag = stack.TryPop(out result);
Console.WriteLine("移除的值为:{0}", result); } #endregion
03-ConcurrentQueue { Console.WriteLine("---------------- 03-ConcurrentQueue ---------------------"); ConcurrentQueue<int> queue = new ConcurrentQueue<int>(); queue.Enqueue(1); queue.Enqueue(2); queue.Enqueue(33); //队列的模式,先进先出,0代表第一个插入的值 var result = 0; //flag为true,表示移除成功,并且返回被移除的值 var flag = queue.TryDequeue(out result);
Console.WriteLine("移除的值为:{0}", result); } #endregion
04-ConcurrentDictionary { Console.WriteLine("---------------- 04-ConcurrentDictionary ---------------------"); ConcurrentDictionary<int, int> dic = new ConcurrentDictionary<int, int>(); dic.TryAdd(1, 10); dic.TryAdd(2, 11); dic.TryAdd(3, 12); dic.ContainsKey(3); //下面是输出字典中的所有值 foreach (var item in dic) { Console.WriteLine(item.Key + item.Value); } } #endregion
复制代码
代码结果:
更多 C++后台开发技术点知识内容包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,流媒体,音视频开发,Linux 内核,TCP/IP,协程,DPDK 多个高级知识点。
二. 队列的综合案例
上面介绍了四大安全线程集合类和与其对应的不安全的线程集合类,可能你会比较疑惑,到底怎么安全了,那些不安全的集合类怎么能变成安全呢,下面以队列为例,来解决这些疑惑。
1. 测试 Queue 队列并发情况下是不安全的(存在资源竞用的问题),ConcurrentQueue 队列在并发情况下是安全的。
2. 利用 Lock 锁+Queue 队列,实现多线程并发情况下的安全问题,即等同于 ConcurrentQueue 队列的效果。
经典案例测试:开启 100 个线程进行入队操作,正常所有的线程执行结束后,队列中的个数应该为 100.
①. Queue 不加锁的情况:结果出现 99、98、100,显然是出问题了。
{ Queue queue = new Queue(); object o = new object(); int count = 0; List<Task> taskList = new List<Task>(); for (int i = 0; i < 100; i++) { var task = Task.Run(() => { queue.Enqueue(count++); }); taskList.Add(task); }
Task.WaitAll(taskList.ToArray()); //发现队列个数在不加锁的情况下 竟然不同 有100,有99 Console.WriteLine("Queue不加锁的情况队列个数" + queue.Count); }
复制代码
②. Queue 加锁的情况:结果全是 100,显然是正确的。
1 { 2 Queue queue = new Queue(); 3 object o = new object(); 4 int count = 0; 5 List<Task> taskList = new List<Task>(); 6 for (int i = 0; i < 100; i++) 7 { 8 var task = Task.Run(() => 9 {10 lock (o)11 {12 queue.Enqueue(count++);13 }14 });15 taskList.Add(task);16 }17 18 Task.WaitAll(taskList.ToArray());19 //发现队列个数在全是10020 Console.WriteLine("Queue加锁的情况队列个数" + queue.Count);21 }
复制代码
③. ConcurrentQueue 不加锁的情况:结果全是 100,显然是正确,同时证明 ConcurrentQueue 队列本身就是线程安全的。
1 { 2 ConcurrentQueue<int> queue = new ConcurrentQueue<int>(); 3 object o = new object(); 4 int count = 0; 5 List<Task> taskList = new List<Task>(); 6 7 for (int i = 0; i < 100; i++) 8 { 9 var task = Task.Run(() =>10 {11 queue.Enqueue(count++);12 });13 taskList.Add(task);14 }15 Task.WaitAll(taskList.ToArray());16 //发现队列个数不加锁的情形=也全是100,证明ConcurrentQueue是线程安全的17 Console.WriteLine("ConcurrentQueue不加锁的情况下队列个数" + queue.Count);18 }
复制代码
3. 在实际项目中,如果使用队列来实现一个业务,该队列需要是全局的,这个时候就需要使用单例(ps:单例是不允许被实例化的,可以通过单例类中的属性或者方法的形式来获取这个类),同时,队列的入队和出队操作,如果使用 Queue 队列,需要配合 lock 锁,来解决多线程下资源的竞用问题。
经典案例:开启 100 个线程对其进行入队操作,然后主线程输入队列的个数,并且将队列中的内容输出.
结果:队列的个数为 100,输出内容 1-100 依次输出。
单例类
1 /// <summary> 2 /// 单例类 3 /// </summary> 4 public class QueueUtils 5 { 6 /// <summary> 7 /// 静态变量:由CLR保证,在程序第一次使用该类之前被调用,而且只调用一次 8 /// </summary> 9 private static readonly QueueUtils _QueueUtils = new QueueUtils();10 11 /// <summary>12 /// 声明为private类型的构造函数,禁止外部实例化13 /// </summary>14 private QueueUtils()15 {16 17 }18 /// <summary>19 /// 声明属性,供外部调用,此处也可以声明成方法20 /// </summary>21 public static QueueUtils instanse22 {23 get24 {25 return _QueueUtils;26 }27 }28 29 30 //下面是队列相关的31 Queue queue = new Queue();32 33 private static object o = new object();34 35 public int getCount()36 {37 return queue.Count;38 }39 40 /// <summary>41 /// 入队方法42 /// </summary>43 /// <param name="myObject"></param>44 public void Enqueue(object myObject)45 {46 lock (o)47 {48 queue.Enqueue(myObject);49 }50 }51 /// <summary>52 /// 出队操作53 /// </summary>54 /// <returns></returns>55 public object Dequeue()56 {57 lock (o)58 {59 if (queue.Count > 0)60 {61 return queue.Dequeue();62 }63 }64 return null;65 }66 67 }
复制代码
出入队操作
1 { 2 int count = 1; 3 List<Task> taskList = new List<Task>(); 4 for (int i = 0; i < 100; i++) 5 { 6 var task = Task.Run(() => 7 { 8 QueueUtils.instanse.Enqueue(count++); 9 });10 taskList.Add(task);11 }12 13 Task.WaitAll(taskList.ToArray());14 //发现队列个数在全是10015 Console.WriteLine("单例模式下队列个数" + QueueUtils.instanse.getCount());16 17 //下面是出队相关的业务18 while (QueueUtils.instanse.getCount() > 0)19 {20 Console.WriteLine("出队:" + QueueUtils.instanse.Dequeue());21 }22 }
复制代码
。。。。。。。。。。。
三. 常见的几类性能调优
PS:
1. 常见的一级事件:CPU 占用过高、死锁问题、内存爆满
a. CPU 过高:查看是否 while(true)中的业务过于复杂,导致 cpu 一直在高负荷运行。
b. 死锁问题:乱用 lock,千万不要 lock 中再加 lock,多个 lock 重叠
c. 内存爆满:字符串的无限增长,全局的静态变量过多。
2. 补充几个常用的性能调优的方式
a. 使用字典类型 Dictionary<T,T>,代替只有两个属性的对象或匿名对象。
b. 使用数组代替只有两个属性的对象或匿名对象。
比如:
index:存放 id
value:存放数量或其他属性
3. 返璞归真,使用最原始的代码代替简洁漂亮的代码。
4. 合理的使用多线程,业务复杂的尽可能的并发执行(或者异步)。
5. 运用设计模式,使代码简洁、易于扩展。
原文链接:第十四节: 介绍四大并发集合类并结合单例模式下的队列来说明线程安全和非安全的场景及补充性能调优问题。
评论