江隅 發表於 2025-12-14 21:40:00

推荐一种并发线程中资源同步常用方法

<p>在实际应用开发中,为了提高效率,一些大的任务会被拆成很多小的子任务,然后再将任务按照先后顺序进行排列组合,而某些可以同时执行的任务,就会被安排成并行执行,进而就会用到多线程去处理;这些并发线程,有时会需要使用同一种资源,且这种资源在同一时刻也只能供少量或单一线程使用,这种资源被称为临界资源。那如何才能保证在并发线程中,各个线程都能有条不紊的使用临界资源呢?我们需要给临界资源一个信号量(Semaphore),当资源正在被占用时,告诉其他后面的线程,需要等待前面的线程使用资源结束,才能接着使用,总而言之,就是需要排队使用资源。</p>
<p>今天我们以一些简单的小例子,简述在.NET开发中信号量的使用方法与应用,仅供学习分享使用,如有不足之处,还请指正。</p>
<p>&nbsp;</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202512/1068941-20251212214106510-1277528631.png"></p>
<p>&nbsp;</p>
<h1>什么是信号量?</h1>
<p>&nbsp;</p>
<p>在 C# 中,信号量(Semaphore)是一种用于线程同步的机制,能够控制对共享资源的访问。它的工作原理是通过维护一个计数器来控制对资源的访问次数。它常用于限制对共享资源(如数据库连接池、<span class="mod-overview__keyword">文件系统</span>、网络资源等)的并发访问。</p>
<p>信号量理解起来有三点核心要素:</p>
<ol>
<li>计数:信号量的核心是一个计数器,表示当前资源的可用数量;即总资源数-正在被使用资源数=剩余可用数量。</li>
<li>等待:当线程请求资源时,此次如果计数器大于0,则线程可以继续执行,同时计数器减1;如果计数器等于0,则线程被阻塞直至其他线程释放资源,即有线程增加计数器的值;</li>
<li>释放:当线程使用完资源后,则需要释放信号量,同时计数器加1,并唤醒其他等待的线程;</li>
</ol>
<p>通过信号量的定义和核心要素,以及实现原理,可以推断出它的应用场景,就是控制对共享临界资源的使用,避免出现过度使用资源的现象出现。</p>
<p>&nbsp;</p>
<p>在.NET开发中,基于C#语言,有两种信号量实现方式,具体如下所示:</p>
<ul>
<li>Semaphore:是基于系统内核实现,属于内核级别同步,支持跨进程资源同步,因此性能较低,内存占用较大;它可以一次释放多个信号量,但是没有提供原生的异步支持;</li>
<li>SemaphoreSlim:是用户级别同步,并不依赖系统内核,因此不支持跨进程资源同步,因此性能更高,内存占用更低;它一次只能释放一个信号量,但是提供了原生异步支持;</li>
</ul>
<p>&nbsp;</p>
<p>具体对比如下图所示:</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202512/1068941-20251212214030000-1009187746.png"></p>
<p>&nbsp;</p>
<h1>Semaphore实例</h1>
<p>&nbsp;</p>
<p>Semaphore主要方法有以下几个:</p>
<ul>
<li>构造方法,Semaphore提供在构造方法有3个,可以根据需要用来设置初始信号数量,最大信号数量,信号量名称,out修饰的是否创建新的信号。如果不需要跨进程,则采集两个默认两个参数在构造函数即可。</li>
<li>WaitOne,等待方法用于等待一个信号,此方法接收一个参数用于设置超时时间,它返回一个bool值,true表示接收到信号,false表示没有接收到信号。</li>
<li>Release,释放资源方法,默认释放一个资源的占用。此方法接收一个参数用于设置释放占用资源的数量。</li>
</ul>
<p>&nbsp;</p>
<p>应用示例:定义一个共享资源信号量,同一时刻允许2个线程使用,最大线程数量为5,则采用默认构造函数并指定初始值,最大值即可,如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>namespace Okcoder.DemoSemaphore
{
    public class Test
    {
      private readonly Semaphore semaphore;

      public Test()
      {
            semaphore = new Semaphore(2, 5);
      }

      /// &lt;summary&gt;
      /// Work
      /// &lt;/summary&gt;
      /// &lt;param name="id"&gt;&lt;/param&gt;
      public void Work(int id)
      {
            try
            {
                Console.WriteLine($"id = {id} 正在等待进入 working area.");
                semaphore.WaitOne();
                Console.WriteLine($"id = {id} 进入了working area.");
                //do something
                Thread.Sleep(3000);
            }
            catch
            {
                throw;
            }
            finally
            {
                Console.WriteLine($"id = {id} 离开了working area.");
                semaphore.Release();
            }
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>当我们创建5个线程,同时调用资源时,也只允许同一时刻2个线程使用,如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>using Okcoder.DemoSemaphore;
using System;

namespace ConsoleAppWithoutTopLevelStatements
{
    class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("Hello, World!");
            Test test = new Test();
            for (int i = 0; i &lt; 5; i++)
            {
                int id = i;
                Task.Run(() =&gt;
                {
                  test.Work(id);
                });
            }
            Console.ReadKey();
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>运行实例,效果如下所示:</p>
<p>&nbsp;</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202512/1068941-20251212220902996-955741480.png"></p>
<p>&nbsp;</p>
<h1>命名信号量</h1>
<p>&nbsp;</p>
<p>Semaphore是内核级别同步,在构造函数中,可以为信号量指定名称,这样就可以跨进程获取信号量,并进行操作。如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>namespace Okcoder.DemoSemaphore
{
    public class Test3
    {
      private readonly Semaphore semaphore;

      public Test3()
      {
            semaphore = new Semaphore(1, 1,"okcoder");
      }

      /// &lt;summary&gt;
      /// Work
      /// &lt;/summary&gt;
      /// &lt;param name="id"&gt;&lt;/param&gt;
      public void Work(int id)
      {
            Console.WriteLine($"id = {id} 正在等待进入 working area.");
            semaphore.WaitOne();
            Console.WriteLine($"id = {id} 进入了working area.");
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>同样运行5个线程调用方法,如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>using Okcoder.DemoSemaphore;
using System;

namespace ConsoleAppWithoutTopLevelStatements
{
    class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("Hello, World!");
            Test3 test = new Test3();
            for (int i = 0; i &lt; 5; i++)
            {
                int id = i;
                Task.Run(() =&gt;
                {
                  test.Work(id);
                });
            }
            Console.ReadKey();
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>在上述示例中,指定了一个初始信号数量为1,最大信号量为1,名称为okcoder的信号量,接下在另外一个程序中通过Semaphore.OpenExisting方法进行获取信号量,并操作信号量的释放,如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>using System;

namespace ConsoleAppWithoutTopLevelStatements2
{
    class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("输入回车键,释放信号量");
            Semaphore semaphore = Semaphore.OpenExisting("okcoder");
            for (int i = 0; i &lt; 5; i++)
            {
                if (Console.ReadKey().Key == ConsoleKey.Enter)
                {
                  semaphore.Release();
                  Console.WriteLine("释放一个信号量");
                }
            }
            Console.WriteLine("信号量已释放完成,按任意键退出");
            Console.ReadKey();
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>运行实例如下所示:</p>
<p>&nbsp;</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202512/1068941-20251212222232656-1448109076.gif"></p>
<p>&nbsp;</p>
<p>在上述示例中,每输入一个Enter键,就释放一个信号量,原进程就允许进入一个资源,说明Semaphore可以被跨进程获取和操作,进而证明它是系统内核级别的同步。</p>
<p>&nbsp;</p>
<h1>SemaphoreSlim实例</h1>
<p>&nbsp;</p>
<p>SemaphoreSlim是Semaphore的一个轻量级实现,它是用户级别的,它允许在同一个进程内各线程之间进行资源的同步。SemaphoreSlim只需要指定初始线程数量,最大线程数量即可,不需要指定信号量的名称。</p>
<p>SemaphoreSlim主要方法有以下几个:</p>
<ul>
<li>构造方法,SemaphoreSlim提供在构造方法有2个,可以根据需要用来设置初始信号数量,最大信号数量。</li>
<li>Wait,等待方法用于等待一个信号,此方法接收一个参数用于设置超时时间,它返回一个bool值,true表示接收到信号,false表示没有接收到信号。</li>
<li>WaitAsync,支持Wait信号的异步调用。</li>
<li>Release,释放资源方法,默认释放一个资源的占用。此方法接收一个参数用于设置释放占用资源的数量。</li>
</ul>
<p>说明:如果不需要跨进程处理资源同步的时候,SemaphoreSlim才是最佳选择。</p>
<p>&nbsp;</p>
<p>应用示例:定义一个共享资源信号量,同一时刻允许2个线程使用,最大线程数量为5,则采用默认构造函数并指定初始值,最大值即可,如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>namespace Okcoder.DemoSemaphore
{
    public class Test2
    {
      private readonly SemaphoreSlim semaphoreSlim;

      public Test2()
      {
            semaphoreSlim = new SemaphoreSlim(2, 5);
      }

      /// &lt;summary&gt;
      /// Work
      /// &lt;/summary&gt;
      /// &lt;param name="id"&gt;&lt;/param&gt;
      public void Work(int id)
      {
            try
            {
                Console.WriteLine($"id = {id} 正在等待进入 working area.");
                semaphoreSlim.Wait();
                Console.WriteLine($"id = {id} 进入了working area.");
                //do something
                Thread.Sleep(3000);
            }
            catch
            {
                throw;
            }
            finally
            {
                Console.WriteLine($"id = {id} 离开了working area.");
                semaphoreSlim.Release();
            }
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>当我们创建5个线程,同时调用资源时,也只允许同一时刻2个线程使用,如下所示:</p>
<p>&nbsp;</p>
<pre class="language-csharp highlighter-hljs"><code>using Okcoder.DemoSemaphore;
using System;

namespace ConsoleAppWithoutTopLevelStatements
{
    class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("Hello, World!");
            Test2 test = new Test2();
            for (int i = 0; i &lt; 5; i++)
            {
                int id = i;
                Task.Run(() =&gt;
                {
                  test.Work(id);
                });
            }
            Console.ReadKey();
      }
    }
}</code></pre>
<p>&nbsp;</p>
<p>运行实例,效果如下所示:</p>
<p>&nbsp;</p>
<p><img src="https://img2024.cnblogs.com/blog/1068941/202512/1068941-20251212223702463-1946128900.png"></p>
<p>可以看到,每次进入共享资源的顺序都不同,但同一时刻允许进入的数量是一样的。</p>
<p>&nbsp;</p>
<p>以上就是《推荐一种并发线程中资源同步常用方法》的全部内容,旨在抛砖引玉,一起学习,共同进步。</p>

</div>
<div id="MySignature" role="contentinfo">
    <div id="AllanboltSignature">

    <p style="border-top: #e0e0e0 1px dashed; border-right: #e0e0e0 1px dashed; border-bottom: #e0e0e0 1px dashed; border-left: #e0e0e0 1px dashed; padding-top: 10px; padding-right: 10px; padding-bottom: 10px; padding-left: 30px; font-family: 微软雅黑; font-size: 12px" id="PSignature">
<br>

   <img alt="" src="https://images.cnblogs.com/cnblogs_com/hsiang/1154298/o_115f1cd8.jpg" width="80px" height="80px">
   
    作者:老码识途
    <br>
    出处:http://www.cnblogs.com/hsiang/
    <br>
    本文版权归作者和博客园共有,写文不易,支持原创,欢迎转载【点赞】,转载请保留此段声明,且在文章页面明显位置给出原文连接,谢谢。
    <br>关注个人公众号,定时同步更新技术及职场文章
<br><br>
   </p>
</div><br><br>
来源:https://www.cnblogs.com/hsiang/p/19343802
頁: [1]
查看完整版本: 推荐一种并发线程中资源同步常用方法