zoukankan      html  css  js  c++  java
  • 聊聊.net 并发控制,lock,Monitor,Semaphore,BlockingQueue,乐观锁串讲

    面试(对,最近在找工作面试...)被问到,.net 并发控制怎么做,BlockingQueue和ConcurrentQueue有什么区别?

    多线程问题的核心是控制对临界资源的访问,接下来我们聊聊.net并发控制,可能除了第一个”lock”,对于其他的几个概念都很陌生,那么这篇文章应该对你有帮助。

    lock

    Monitor

    Semaphore

    ConcurrentQueue

    BlockingQueue

    BlockingCollection

    一、lock

    说到并发控制,我们首先想到的肯定是 lock关键字。

    这里要说一下,lock锁的究竟是什么?是lock下面的代码块吗,不,是locker对象。

    我们想象一下,locker对象相当于一把门锁(或者钥匙),后面代码块相当于屋里的资源。

    哪个线程先控制这把锁,就有权访问代码块,访问完成后再释放权限,下一个线程再进行访问。

    注意:如果代码块中的逻辑执行时间很长,那么其他线程也会一直等下去,直到上一个线程执行完毕,释放锁。

     1         object locker = new object();
     2 
     3         private void Add()
     4         {
     5             lock (locker)
     6             {
     7                 Thread.Sleep(1000);
     8                 counter++;
     9                 this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Add counter={counter}.");
    10             }
    11         }

    二、Moniter

    Monitor是一个静态类(System.Threading.Monitor),功能与lock关键字基本一样,也是加锁,控制并发。

    有两个重要的方法:

    Monitor.Enter()  //获取一个锁

    Monitor.Exit()   //释放一个锁 

    另外几个方法:

    public static bool TryEnter(object obj, int millisecondsTimeout)  //相比于 public static void Enter(object obj) 方法,多了超时时间设置,如果等待超过一定时间,就不再等待了,另外,只有TryEnter()返回值为true时,才能进入代码块。

    public static bool Wait(object obj, int millisecondsTimeout)    //这个方法在已经获得锁权限的代码块中调用时,或暂时释放锁,等待一定时间后,重新获取锁权限,继续执行Wait后面的代码。(真想不明怎么会有这种相互礼让的操作)

    public static void Pulse(object obj)      //这个方法的解释是,通知在等待队列中的线程,锁对象状态改变。(测试发现,此方法并不会真正改变锁定状态,只是通知的作用)

     TryEnter代码示例:

     1         int counter = 0;
     2         object locker = new object();
     3 
     4         private void Minus()
     5         {
     6             //加上try -catch-finally,防止由于异常,锁无法释放,这也是为什么我们更多使用lock而不是Moniter的原因。
     7             try
     8             {
     9                 //只有TryEnter()返回值为true时,才能进入代码块,与Enter()方法不一样
    10                 if (Monitor.TryEnter(locker, 5000))
    11                 {
    12                     this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Minus in");
    13                     Thread.Sleep(1000);
    14                     counter--;
    15                     this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Minus counter={counter}.");
    16                 }
    17             }
    18             catch (Exception ex)
    19             {
    20                 this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Minus Exception {ex.Message}");
    21             }
    22             finally
    23             {
    24                 Monitor.Exit(locker);
    25             }
    26         }

      

    通过上面的代码,我们可以看出Monitor和lock实现的功能基本一致,但Monitor的使用要明显比lock更复杂,也行这就是我们平时更多的使用lock,而不是Monitor的原因。

    三、Semaphore 信号量

    System.Threading.Semaphore 

    lock和Monitor加锁之后,每次只能有一个线程访问临界代码,信号量类似于一个线程池,线程访问之前获取一个信号,访问完成释放信号,只要信号量内有可用信号便可以访问,否则等待。

    构造函数:

    public Semaphore(int initialCount, int maximumCount)  //创建一个信号量,指定初始信号数量和最大信号数量。

    几个重要方法:

    public int Release()        //代码注释的意思是:退出信号量,并返回之前的(可用信号)数量。实际上,除了退出,这个方法每调用一次会增加一个可用信号,但数量达到最大数量时会抛异常。

    public int Release(int releaseCount)   //和上面的方法类似,上面的方法每次只释放一个信号,这个方法可以指定信号数量。

    public virtual bool WaitOne()    //等待一个可用信号

    看下面的示例代码,如果只初始一个信号量,new Semaphore(1, 100),运行结果与lock和Monitor是一样的,两个方法交替执行,如果初始信号量为多个时,new Semaphore(3, 100),执行效率高的方法要占用更多的信号,从而执行更多次。

     1         int counter = 0;
     2         int semaphoreCount = 0;
     3         Semaphore semaphore = new Semaphore(3, 100);
     4 
     5         private void Add()
     6         {
     7             semaphore.WaitOne();
     8             Thread.Sleep(1000);
     9             counter++;
    10             semaphoreCount = semaphore.Release();
    11             this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Add counter={counter}.SemaphoreCount:{semaphoreCount}");
    12         }
    13 
    14         private void Minus()
    15         {
    16             semaphore.WaitOne();
    17             Thread.Sleep(2000);
    18             counter--;
    19             semaphore.Release();
    20             this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Minus counter={counter}.SemaphoreCount:{semaphoreCount}");
    21         }

    Semaphore在生产者/消费者模式下的应用

    生产者每次添加一个信号,消费者每次消耗一个信号,如果信号量为0,则消费者进入等待状态。

     1         int counter = 0;
     2         int semaphoreCount = 0;
     3         Semaphore semaphore = new Semaphore(0, int.MaxValue);
     4 
     5         private void Product()
     6         {
     7             semaphoreCount = semaphore.Release();
     8             Thread.Sleep(1000);
     9             counter++;
    10             this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Product counter={counter}.SemaphoreCount:{semaphoreCount}");
    11         }
    12 
    13         private void Consume()
    14         {
    15             semaphore.WaitOne();
    16             Thread.Sleep(2000);
    17             counter--;
    18             this.logger.LogDebug($"{DateTime.Now.ToLongTimeString()} Consume counter={counter}.SemaphoreCount:{semaphoreCount}");
    19         }

      

    四、ConcurrentQueue 和 Queue

    .net 集合中有一类线程安全的集合 System.Collections.Concurrent,ConcurrentQueue 就是其中的一个,线程安全的队列,有普通队列Queue先进先出的特点,同时又具备多线程安全。

    测试过程中发现:

    Queue 类的两个出队列方法 Dequeue()TryDequeue(out result),在多线程环境下,Dequeue() 会出现并发访问错误,但TryDequeue(out result)不会,即TryDequeue(out result)即使不加锁,在多线程环境下也运行正常。

    ConcurrentQueue 类只有一个出队列方法 TryDequeue(out result),当然,是线程安全的。

    五、BlockingQueue

    BlockingQueue并不是.net内置的类,如果有人问这个类,那么他多半是在说BlockingCollection

    关于 BlockingQueue 有一篇很不错的文章,可以参考一下:

    https://docs.microsoft.com/zh-cn/archive/blogs/toub/blocking-queues

    六、BlockingCollection

    BlockingCollection是.net内置的类,相当于带有阻塞功能的 ConcurrentQueue ,数据先进先出,相比较ConcurrentQueue ,BlockingCollection在从队列中读取数据时,如果队列为空,那么它会等待(block),直到有数据可读取。

    而ConcurrentQueue ,需要我们自行判断是否读取了数据,并且控制循环读取的频率。

    .net 文档对这个类解释的非常详细,可以仔细阅读:

    https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent.blockingcollection-1?view=netcore-3.1

    七、乐观锁

    前面讲的这些,都是属于.net提供的并发控制方案,还有另一种更常用的并发控制方式,乐观锁。

    乐观锁本质上并不是加锁,而是数据版本控制。乐观锁的出发点是假定并发错误发生的概率很小,从而允许程序并发执行。

    首先,数据要有一个版本号,每次数据更新,要产生一个新的版本号。

    其次,进入数据处理逻辑之前,记录该数据的版本号,数据处理结束后,重新读取数据,比较前后两个版本号是否一致,如果一致,则提交,处理完成,如果不一致,说明产生了并发错误,则抛出异常或已其他方式终止程序执行,从而保证数据的一致性。

     总结

    lock是最常用的并发控制方式,Monitor的功能与lock类似,但使用复杂,非必须不建议使用。

    Semaphore,信号量,是一个不错的功能,特定应用场景下非常实用。

    ConcurrentQueue 是一个线程安全的队列,在多线程并发环境下使用,可避免由于并发引起的错误。(我们可以使用lock+Queue,实现ConcurrentQueue,自己感兴趣可以试一下)

    BlockingCollection 带阻塞功能的 ConcurrentQueue ,没有可用数据的情况下,进入等待状态,防止循环访问,减少CPU资源浪费。(我们可以通过Semaphore+ConcurrentQueue ,实现BlockingCollection ,自己感兴趣可以试一下)

       最后,祝大家祝编程快乐。

  • 相关阅读:
    spin_count
    not in改写关联无需考虑重复数据
    substr函数
    Flex样式-ToolTip篇
    Flex样式-ProgressBar篇
    Flex样式-MenuBar篇
    Flex样式-DataGrid篇
    Flex样式-ColorPicker篇
    FireBug之Console命令大全
    Flex样式-VSlider篇
  • 原文地址:https://www.cnblogs.com/flame7/p/13670180.html
Copyright © 2011-2022 走看看