并发编程 - 线程同步(九)之信号量Semaphore

打印 上一主题 下一主题

主题 917|帖子 917|积分 2751

前面对自旋锁SpinLock进行了详细学习,今天我们将学习另一个种同步机制——信号量Semaphore。

01、信号量是什么?

在 C# 中,信号量(Semaphore)是一种用于线程同步的机制,能够控制对共享资源的访问。它的工作原理是通过维护一个计数器来控制对资源的访问次数。它常用于限制对共享资源(如数据库连接池、文件系统、网络资源等)的并发访问。
1、信号量有三个核心概念:

1.计数:信号量的核心是一个计数器,表示当前资源的可用数量;
2.等待:当线程哀求资源时,此次如果计数器大于0,则线程可以继续执行,同时计数器减1;如果计数器即是0,则线程被壅闭直至其他线程开释资源,即有线程增加计数器的值;
3.开释:当线程使用完资源后,则必要开释信号量,同时计数器加1,并唤醒其他等待的线程;
相信理解了信号量核心概念,其工作原理就不言而喻。
2、应用场景:

通过对信号量的工作原理了解,我们可以总结为:信号量就是为了控制对共享资源的访问,包管共享资源不会被过度使用,因此可以引申出以下实用于信号量的场景:
1.控制各种连接池:限制同时打开各种资源的连接数量(比如连接打印机数量,数据库连接数据,文件访问数量);
2.限制网络哀求:防止服务器过载,导致服务器崩溃;
3.协调多个线程的执行顺序:通过信号量控制生成者和消费者之间的资源访问, 实现生产者和消费者模子;
02、C#中的信号量实现

C#提供了两种信号量类型:Semaphore和SemaphoreSlim。其中两者功能基本相同,却又有所不同,而SemaphoreSlim是更轻量、更快速的信号量实现。下面是两种简朴比力:
Semaphore: 是基于系统内核实现,属于内核级别同步,支持跨进程资源同步,因此性能较低,内存占用较大;它可以一次开释多个信号量,但是没有提供原生的异步支持;
SemaphoreSlim: 是用户级别同步,并不依赖系统内核,因此不支持跨进程资源同步,因此性能更高,内存占用更低;它一次只能开释一个信号量,但是提供了原生异步支持;

03、Semaphore使用示例

通过对信号量原理的详细了解,而作为对信号量实现类Semaphore,这些原理也同样实用,因此Semaphore类的构造函数就指定了用于控制线程数量的参数。其构造函数如下:
  1. public Semaphore(int initialCount, int maximumCount);
  2. public Semaphore(int initialCount, int maximumCount, string name);
复制代码
initialCount: 初始化信号量的计数,表示初始时可以同时访问资源的线程数量。
maximumCount: 信号量的最大计数,表示允许同时访问资源的最大线程数。
name: 可选的名称,用于命名信号量对象(可在多个进程间共享信号量)。
然后可以用WaitOne方法获取信号量,使用Release方法开释信号量。
下面我们做一个小例子,创建一个初始化为2个线程的信号量Semaphore,然后启动5个线程用来访问信号量,并在获取信号量之前、之后以及开释信号量之前都加上日志,用来观察线程被控制的过程。代码如下:
  1. public class SemaphoreExample
  2. {
  3.     //初始化最多2个线程同时进入, 最大允许3个线程
  4.     private static Semaphore semaphore = new Semaphore(2, 3);
  5.     //用于不同的线程显示不同的颜色,方便观察结果
  6.     private static ConsoleColor[] colors = new ConsoleColor[5]
  7.     {
  8.         ConsoleColor.Red,
  9.         ConsoleColor.White,
  10.         ConsoleColor.Yellow,
  11.         ConsoleColor.Green,
  12.         ConsoleColor.Blue
  13.     };
  14.     public static void Worker(object? i)
  15.     {
  16.         var id = (int)i;
  17.         var color = colors[id];
  18.         PrintText.SafeForegroundColor($"线程 {id} 等待进入...", color);
  19.         //请求进入信号量(如果资源不可用,则返回)
  20.         semaphore.WaitOne();
  21.         PrintText.SafeForegroundColor($"线程 {id} 已 [ 进入 ] 同步代码块.", color);
  22.         //业务处理
  23.         Thread.Sleep(2000);
  24.         PrintText.SafeForegroundColor($"线程 {id} 已 [ 离开 ] 同步代码块.", color);
  25.         //释放信号量(让其他线程可以进入)
  26.         semaphore.Release();
  27.     }
  28. }
  29. public static void SemaphoreRun()
  30. {
  31.     for (int i = 0; i < 5; i++)
  32.     {
  33.         Thread t = new Thread(SemaphoreExample.Worker);
  34.         t.Start(i);
  35.     }
  36. }
复制代码
我们一起看看执行结果:

可以发现在红框部分为所有线程初始化完成,同时只有两个线程获取到信号量,之后就是又一个信号量开释成功,则紧跟着一个等待线程会立马进入,直到所有线程处理完成。
到这里会有一个疑问,我们在初始化信号量Semaphore时,指定了最大允许3个线程可以同时进入,但是上面示例并没有表现出来,这是为什么呢?
这是因为在信号量的生命周期中,maximumCount参数并不会直接改变信号量的当前可用资源数量,而是限制并发线程数的最大值。因此如果要想看到效果,则必要经过特殊处理才行,比如我们在上面的代码中开释信号量时,我们执行两次开释操作,但是这样会导致末了开释操作报SemaphoreFullException异常,因此要注意获取和开释信号量操作要配对,这里仅仅为了演示,执行结果如下:

可以看到maximumCount最大访问线程数生效了。
04、Semaphore使用注意事项

1.确保每个调用 WaitOne方法 的线程终极都会调用 Release方法。否则,可能会导致死锁。
2.确保Release方法的调用次数不应超过 WaitOne方法的调用次数,否则会抛出 SemaphoreFullException异常
3.如果单进程步调尽量选择SemaphoreSlim,因为SemaphoreSlim性能更好。
4.如果必要跨进程同步可以使用带名称的构造函数 Semaphore(int initialCount, int maximumCount, string name)。
:测试方法代码以及示例源码都已经上传至代码库,有爱好的可以看看。https://gitee.com/hugogoos/Planner

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表