C#:高效的线程安全队列ConcurrentQueue<T>

讲到线程安全队列之前先说一下线程安全与线程不安全

  1. 线程安全:多个线程在执行同一段代码的时候采用加锁机制,使每次的执行结果和单线程执行的结果都是一样的
  2. 线程不安全:不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
    举例:购票系统有1000张票。A线程和B线程同时抢票,有时候会抢到同一张票。这是就是线程不安全。两个线程可能同时抢一个数据。这时候就要用到锁了

线程安全队列:ConcurrentQueue<T>的使用
NuGet 安装包 > System.Collections.Concurrent

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
ConcurrentQueue<SellTicket> cqsellTickets = new ConcurrentQueue<SellTicket>();

SellTicket sellTicket=new SellTicket();

List<SellTicket> sellTickets = new List<SellTicket>();

sellTickets.Add(new SellTicket
            {
                name = "张三",
                count="011A"
            });
{
                name = "李四",
                count="011B"
            });

cqsellTickets.Enqueue(sellTickets[0]);//单条入队
Console.WriteLine(cqsellTickets.Count);
            //输出为:1
cqsellTickets.TryDequeue(out sellTicket);//单条出队
Console.WriteLine(cqsellTickets.Count);
            //输出为:0
//这条数据出队了后cqsellTickets里面是没有这个数据了

Console.WriteLine("购票人姓名:"+ sellTicket.name+ "座位号:"+ sellTicket.count);
            //输出为: 购票人姓名:张三座位号:011A


foreach (var item in sellTickets)
  {
        cqsellTickets.Enqueue(item);//多条入队
  }
            
Console.WriteLine(cqsellTickets.Count);
            //输出为:2

foreach (var item in cqsellTickets)
    {
        cqsellTickets.TryDequeue(out sellTicket);//多条出队
        Console.WriteLine("购票人姓名:"+ sellTicket.name+ "座位号:"+ sellTicket.count);
            ////输出为: 购票人姓名:张三座位号:011A
            ////输出为: 购票人姓名:李四座位号:011B
   }

Console.WriteLine(cqsellTickets.Count);
            //输出为:0

            

class SellTicket
    {
        public string name { get; set; }
        public string count { get; set; }
    }

ConcurrentQueue进队出队已经讲解完了。然后我们结合多线程看看

多线程购票

static List<SellTicket> sellTickets = new List<SellTicket>();
for (int i = 1; i < 1000; i++)
{
     SellTicket sellTicket = new SellTicket();
     sellTicket.count = i + "";
     sellTicket.name = "张" + i;
     sellTickets.Add(sellTicket);
}
            
//创建两个线程
ThreadStart threadStart1 = new ThreadStart(StartThedad1);
Thread thread1 = new Thread(threadStart1);
ThreadStart threadStart2 = new ThreadStart(StartThedad2);
Thread thread2 = new Thread(threadStart2);
            
thread1.Start();//启动线程1
thread2.Start();//启动线程2
static void StartThedad1() 
{
    while (true)
    {
        if (sellTickets.Count > 0)
       {
           SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
           sellTickets.Remove(sellTicket);//获取到删除数据
           Console.WriteLine("thread1   购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
       }
    }
}

static void StartThedad2() 
{
    while (true)
    {
        if (sellTickets.Count > 0)
       {
           SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
           sellTickets.Remove(sellTicket);//获取到删除数据
           Console.WriteLine("thread2   购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
       }
    }
}

运行如下:


1604825564(1).jpg

数据重复了,而且数据很杂乱

我们加个锁(Lock)来看看效果

private static object dataLock = new object();
static List<SellTicket> sellTickets = new List<SellTicket>();
static void StartThedad1() 
{
lock(dataLock)
{
    while (true)
    {
        if (sellTickets.Count > 0)
       {
           SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
           sellTickets.Remove(sellTicket);//获取到删除数据
           Console.WriteLine("thread1   购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
       }
    }
}
}

static void StartThedad2() 
{
lock(dataLock)
{
    while (true)
    {
        if (sellTickets.Count > 0)
       {
           SellTicket sellTicket = sellTickets.FirstOrDefault();//获取集合里面第一条数据
           sellTickets.Remove(sellTicket);//获取到删除数据
           Console.WriteLine("thread2   购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
       }
    }
}
}

运行结果:


image.png

这样就对了。加上锁后效果很好

不加锁。用ConcurrentQueue安全队列来试试,看看效果如何

static ConcurrentQueue<SellTicket> cqsellTickets = new ConcurrentQueue<SellTicket>();
static void Main(string[] args)
{
      for (int i = 1; i < 1000; i++)
      {
                SellTicket sellTicket = new SellTicket();
                sellTicket.count = i + "";
                sellTicket.name = "张" + i;
                cqsellTickets.Enqueue(sellTicket);//入队
      }
            //创建两个线程
            ThreadStart threadStart1 = new ThreadStart(StartThedad1);
            Thread thread1 = new Thread(threadStart1);
            ThreadStart threadStart2 = new ThreadStart(StartThedad2);
            Thread thread2 = new Thread(threadStart2);
            
            thread1.Start();//启动线程1
            thread2.Start();//启动线程2
}
static void StartThedad1() 
{
    while (true)
    {
        if (cqsellTickets.Count > 0)
       {
           SellTicket sellTicket = new SellTicket();
           cqsellTickets.TryDequeue(out sellTicket);//出队,赋值到sellTicket
           Console.WriteLine("thread1   购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
       }
    }
}

static void StartThedad2() 
{
    while (true)
    {
        if (cqsellTickets.Count > 0)
       {
           SellTicket sellTicket = new SellTicket();
           cqsellTickets.TryDequeue(out sellTicket);//出队,赋值到sellTicket
           Console.WriteLine("thread2   购票人姓名:" + sellTicket.name + "座位号:" + sellTicket.count);
       }
    }
}

运行看看效果如何


image.png

不管有多乱,数据是不会重复的。有的人是不是想说,那我用锁不是更好吗,为什么要用这个线程安全队列。

首先我们知道锁主要有两种,悲观锁和乐观锁。对于悲观锁,它永远会假定最糟糕的情况,就像我们上面说到的互斥机制,每次我们都假定会有其他的线程和我们竞争资源,因此必须要先拿到锁,之后才放心的进行我们的操作,这就使得争夺锁成为了我们每次操作的第一步。乐观锁则不同,乐观锁假定在很多情况下,资源都不需要竞争,因此可以直接进行读写,但是如果碰巧出现了多线程同时操控数据的情况,那么就多试几次,直到成功(也可以设置重试的次数)。

悲观锁:同一时间只有一个线程具有读写的权限。锁的机制在并发量不大情况下,十分的清晰有效。在并发量较大的时候,会因为对锁的竞争而越发不高效。同时,锁本身也需要维护一定的资源,也需要消耗性能。

乐观锁:每次读写都不考虑锁的存在,那么他是如何知道自己这次操作和其他线程是冲突的呢?这就是Lock-free队列的关键——原子操作。原子操作可以保证一次操作在执行的过程中不会被其他线程打断,因此在多线程程序中也不需要同步操作

我们也要像乐观锁一样,编译一次不过,不行两次,两次不行三次。直到编译通过。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。