聊聊.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 ,自己感兴趣可以试一下)

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值