博客 / 詳情

返回

推薦一種併發線程中資源同步常用方法

在實際應用開發中,為了提高效率,一些大的任務會被拆成很多小的子任務,然後再將任務按照先後順序進行排列組合,而某些可以同時執行的任務,就會被安排成並行執行,進而就會用到多線程去處理;這些併發線程,有時會需要使用同一種資源,且這種資源在同一時刻也只能供少量或單一線程使用,這種資源被稱為臨界資源。那如何才能保證在併發線程中,各個線程都能有條不紊的使用臨界資源呢?我們需要給臨界資源一個信號量(Semaphore),當資源正在被佔用時,告訴其他後面的線程,需要等待前面的線程使用資源結束,才能接着使用,總而言之,就是需要排隊使用資源。

今天我們以一些簡單的小例子,簡述在.NET開發中信號量的使用方法與應用,僅供學習分享使用,如有不足之處,還請指正。

 

image

 

什麼是信號量?

 

在 C# 中,信號量(Semaphore)是一種用於線程同步的機制,能夠控制對共享資源的訪問。它的工作原理是通過維護一個計數器來控制對資源的訪問次數。它常用於限制對共享資源(如數據庫連接池、文件系統、網絡資源等)的併發訪問。

信號量理解起來有三點核心要素:

  1. 計數:信號量的核心是一個計數器,表示當前資源的可用數量;即總資源數-正在被使用資源數=剩餘可用數量。
  2. 等待:當線程請求資源時,此次如果計數器大於0,則線程可以繼續執行,同時計數器減1;如果計數器等於0,則線程被阻塞直至其他線程釋放資源,即有線程增加計數器的值;
  3. 釋放:當線程使用完資源後,則需要釋放信號量,同時計數器加1,並喚醒其他等待的線程;

通過信號量的定義和核心要素,以及實現原理,可以推斷出它的應用場景,就是控制對共享臨界資源的使用,避免出現過度使用資源的現象出現。

 

在.NET開發中,基於C#語言,有兩種信號量實現方式,具體如下所示:

  • Semaphore:是基於系統內核實現,屬於內核級別同步,支持跨進程資源同步,因此性能較低,內存佔用較大;它可以一次釋放多個信號量,但是沒有提供原生的異步支持;
  • SemaphoreSlim:是用户級別同步,並不依賴系統內核,因此不支持跨進程資源同步,因此性能更高,內存佔用更低;它一次只能釋放一個信號量,但是提供了原生異步支持;

 

具體對比如下圖所示:

image

 

Semaphore實例

 

Semaphore主要方法有以下幾個:

  • 構造方法,Semaphore提供在構造方法有3個,可以根據需要用來設置初始信號數量,最大信號數量,信號量名稱,out修飾的是否創建新的信號。如果不需要跨進程,則採集兩個默認兩個參數在構造函數即可。
  • WaitOne,等待方法用於等待一個信號,此方法接收一個參數用於設置超時時間,它返回一個bool值,true表示接收到信號,false表示沒有接收到信號。
  • Release,釋放資源方法,默認釋放一個資源的佔用。此方法接收一個參數用於設置釋放佔用資源的數量。

 

應用示例:定義一個共享資源信號量,同一時刻允許2個線程使用,最大線程數量為5,則採用默認構造函數並指定初始值,最大值即可,如下所示:

 

namespace Okcoder.DemoSemaphore
{
    public class Test
    {
        private readonly Semaphore semaphore;

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

        /// <summary>
        /// Work
        /// </summary>
        /// <param name="id"></param>
        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();
            }
        }
    }
}

 

當我們創建5個線程,同時調用資源時,也只允許同一時刻2個線程使用,如下所示:

 

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 < 5; i++)
            {
                int id = i;
                Task.Run(() =>
                {
                    test.Work(id);
                });
            }
            Console.ReadKey();
        }
    }
}

 

運行實例,效果如下所示:

 

image

 

命名信號量

 

Semaphore是內核級別同步,在構造函數中,可以為信號量指定名稱,這樣就可以跨進程獲取信號量,並進行操作。如下所示:

 

namespace Okcoder.DemoSemaphore
{
    public class Test3
    {
        private readonly Semaphore semaphore;

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

        /// <summary>
        /// Work
        /// </summary>
        /// <param name="id"></param>
        public void Work(int id)
        {
            Console.WriteLine($"id = {id} 正在等待進入 working area.");
            semaphore.WaitOne();
            Console.WriteLine($"id = {id} 進入了working area.");
        }
    }
}

 

同樣運行5個線程調用方法,如下所示:

 

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 < 5; i++)
            {
                int id = i;
                Task.Run(() =>
                {
                    test.Work(id);
                });
            }
            Console.ReadKey();
        }
    }
}

 

在上述示例中,指定了一個初始信號數量為1,最大信號量為1,名稱為okcoder的信號量,接下在另外一個程序中通過Semaphore.OpenExisting方法進行獲取信號量,並操作信號量的釋放,如下所示:

 

using System;

namespace ConsoleAppWithoutTopLevelStatements2
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("輸入回車鍵,釋放信號量");
            Semaphore semaphore = Semaphore.OpenExisting("okcoder");
            for (int i = 0; i < 5; i++)
            {
                if (Console.ReadKey().Key == ConsoleKey.Enter)
                {
                    semaphore.Release();
                    Console.WriteLine("釋放一個信號量");
                }
            }
            Console.WriteLine("信號量已釋放完成,按任意鍵退出");
            Console.ReadKey();
        }
    }
}

 

運行實例如下所示:

 

GIF 2025-12-12 22-21-38

 

在上述示例中,每輸入一個Enter鍵,就釋放一個信號量,原進程就允許進入一個資源,説明Semaphore可以被跨進程獲取和操作,進而證明它是系統內核級別的同步。

 

SemaphoreSlim實例

 

SemaphoreSlim是Semaphore的一個輕量級實現,它是用户級別的,它允許在同一個進程內各線程之間進行資源的同步。SemaphoreSlim只需要指定初始線程數量,最大線程數量即可,不需要指定信號量的名稱。

SemaphoreSlim主要方法有以下幾個:

  • 構造方法,SemaphoreSlim提供在構造方法有2個,可以根據需要用來設置初始信號數量,最大信號數量。
  • Wait,等待方法用於等待一個信號,此方法接收一個參數用於設置超時時間,它返回一個bool值,true表示接收到信號,false表示沒有接收到信號。
  • WaitAsync,支持Wait信號的異步調用。
  • Release,釋放資源方法,默認釋放一個資源的佔用。此方法接收一個參數用於設置釋放佔用資源的數量。

説明:如果不需要跨進程處理資源同步的時候,SemaphoreSlim才是最佳選擇。

 

應用示例:定義一個共享資源信號量,同一時刻允許2個線程使用,最大線程數量為5,則採用默認構造函數並指定初始值,最大值即可,如下所示:

 

namespace Okcoder.DemoSemaphore
{
    public class Test2
    {
        private readonly SemaphoreSlim semaphoreSlim;

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

        /// <summary>
        /// Work
        /// </summary>
        /// <param name="id"></param>
        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();
            }
        }
    }
}

 

當我們創建5個線程,同時調用資源時,也只允許同一時刻2個線程使用,如下所示:

 

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 < 5; i++)
            {
                int id = i;
                Task.Run(() =>
                {
                    test.Work(id);
                });
            }
            Console.ReadKey();
        }
    }
}

 

運行實例,效果如下所示:

 

image

可以看到,每次進入共享資源的順序都不同,但同一時刻允許進入的數量是一樣的。

 

以上就是《推薦一種併發線程中資源同步常用方法》的全部內容,旨在拋磚引玉,一起學習,共同進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.