C#多線程系列之資源池限制

Semaphore、SemaphoreSlim 類

兩者都可以限制同時訪問某一資源或資源池的線程數。

這裡先不扯理論,我們從案例入手,通過示例代碼,慢慢深入瞭解。

Semaphore 類

這裡,先列出 Semaphore 類常用的 API。

其構造函數如下:

構造函數 說明
Semaphore(Int32, Int32) 初始化 Semaphore 類的新實例,並指定初始入口數和最大並發入口數。
Semaphore(Int32, Int32, String) 初始化 Semaphore 類的新實例,並指定初始入口數和最大並發入口數,根據需要指定系統信號燈對象的名稱。
Semaphore(Int32, Int32, String, Boolean) 初始化 Semaphore 類的新實例,並指定初始入口數和最大並發入口數,還可以選擇指定系統信號量對象的名稱,以及指定一個變量來接收指示是否創建瞭新系統信號量的值。

Semaphore 使用純粹的內核時間(kernel-time)方式(等待時間很短),並且支持在不同的進程間同步線程(像Mutex)。

Semaphore 常用方法如下:

方法 說明
Close() 釋放由當前 WaitHandle占用的所有資源。
OpenExisting(String) 打開指定名稱為信號量(如果已經存在)。
Release() 退出信號量並返回前一個計數。
Release(Int32) 以指定的次數退出信號量並返回前一個計數。
TryOpenExisting(String, Semaphore) 打開指定名稱為信號量(如果已經存在),並返回指示操作是否成功的值。
WaitOne() 阻止當前線程,直到當前 WaitHandle 收到信號。
WaitOne(Int32) 阻止當前線程,直到當前 WaitHandle 收到信號,同時使用 32 位帶符號整數指定時間間隔(以毫秒為單位)。
WaitOne(Int32, Boolean) 阻止當前線程,直到當前的 WaitHandle 收到信號為止,同時使用 32 位帶符號整數指定時間間隔,並指定是否在等待之前退出同步域。
WaitOne(TimeSpan) 阻止當前線程,直到當前實例收到信號,同時使用 TimeSpan 指定時間間隔。
WaitOne(TimeSpan, Boolean) 阻止當前線程,直到當前實例收到信號為止,同時使用 TimeSpan 指定時間間隔,並指定是否在等待之前退出同步域。

示例

我們來直接寫代碼,這裡使用 《原子操作 Interlocked》 中的示例,現在我們要求,采用多個線程執行計算,但是隻允許最多三個線程同時執行運行。

使用 Semaphore ,有四個個步驟:

new 實例化 Semaphore,並設置最大線程數、初始化時可進入線程數;

使用 .WaitOne(); 獲取進入權限(在獲得進入權限前,線程處於阻塞狀態)。

離開時使用 Release() 釋放占用。

Close() 釋放Semaphore 對象。

《原子操作 Interlocked》 中的示例改進如下:

    class Program
    {
        // 求和
        private static int sum = 0;
        private static Semaphore _pool;

        // 判斷十個線程是否結束瞭。
        private static int isComplete = 0;
        // 第一個程序
        static void Main(string[] args)
        {
            Console.WriteLine("執行程序");

            // 設置允許最大三個線程進入資源池
            // 一開始設置為0,就是初始化時允許幾個線程進入
            // 這裡設置為0,後面按下按鍵時,可以放通三個線程
            _pool = new Semaphore(0, 3);
            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(AddOne));
                thread.Start(i + 1);
            }
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("任意按下鍵(不要按關機鍵),可以打開資源池");
            Console.ForegroundColor = ConsoleColor.White;
            Console.ReadKey();

            // 準許三個線程進入
            _pool.Release(3);

            // 這裡沒有任何意義,就單純為瞭演示查看結果。
            // 等待所有線程完成任務
            while (true)
            {
                if (isComplete >= 10)
                    break;
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
            Console.WriteLine("sum = " + sum);

            // 釋放池
            _pool.Close();
            
        }

        public static void AddOne(object n)
        {
            Console.WriteLine($"    線程{(int)n}啟動,進入隊列");
            // 進入隊列等待
            _pool.WaitOne();
            Console.WriteLine($"第{(int)n}個線程進入資源池");
            // 進入資源池
            for (int i = 0; i < 10; i++)
            {
                Interlocked.Add(ref sum, 1);
                Thread.Sleep(TimeSpan.FromMilliseconds(500));
            }
            // 解除占用的資源池
            _pool.Release();
            isComplete += 1;
            Console.WriteLine($"                     第{(int)n}個線程退出資源池");
        }
    }

看著代碼有點多,快去運行一下,看看結果。

示例說明

實例化 Semaphore 使用瞭new Semaphore(0,3); ,其構造函數原型為

public Semaphore(int initialCount, int maximumCount);

initialCount 表示一開始允許幾個進程進入資源池,如果設置為0,所有線程都不能進入,要一直等資源池放通。

maximumCount 表示最大允許幾個線程進入資源池。

Release() 表示退出信號量並返回前一個計數。這個計數指的是資源池還可以進入多少個線程。

可以看一下下面的示例:

        private static Semaphore _pool;
        static void Main(string[] args)
        {
            _pool = new Semaphore(0, 5);
            _pool.Release(5);
            new Thread(AddOne).Start();
            Thread.Sleep(TimeSpan.FromSeconds(10));
            _pool.Close();
        }

        public static void AddOne()
        {
            _pool.WaitOne();
            Thread.Sleep(1000);
            int count = _pool.Release();
            Console.WriteLine("在此線程退出資源池前,資源池還有多少線程可以進入?" + count);
        }

信號量

前面我們學習到 Mutex,這個類是全局操作系統起作用的。我們從 Mutex 和 Semphore 中,也看到瞭 信號量這個東西。

信號量分為兩種類型:本地信號量和命名系統信號量。

  • 命名系統信號量在整個操作系統中均可見,可用於同步進程的活動。
  • 局部信號量僅存在於進程內。

當 name 為 null 或者為空時,Mutex 的信號量時局部信號量,否則 Mutex 的信號量是命名系統信號量。

Semaphore 的話,也是兩種情況都有。

如果使用接受名稱的構造函數創建 Semaphor 對象,則該對象將與該名稱的操作系統信號量關聯。

兩個構造函數:

Semaphore(Int32, Int32, String)
Semaphore(Int32, Int32, String, Boolean)

上面的構造函數可以創建多個表示同一命名系統信號量的 Semaphore 對象,並可以使用 OpenExisting 方法打開現有的已命名系統信號量。

我們上面使用的示例就是局部信號量,進程中引用本地 Semaphore 對象的所有線程都可以使用。 每個 Semaphore 對象都是單獨的本地信號量。

SemaphoreSlim類

SemaphoreSlim 跟 Semaphore 有啥關系?

微軟文檔:

SemaphoreSlim 表示對可同時訪問資源或資源池的線程數加以限制的 Semaphore 的輕量替代。

SemaphoreSlim 不使用信號量,不支持進程間同步,隻能在進程內使用。

它有兩個構造函數:

構造函數 說明
SemaphoreSlim(Int32) 初始化 SemaphoreSlim 類的新實例,以指定可同時授予的請求的初始數量。
SemaphoreSlim(Int32, Int32) 初始化 SemaphoreSlim 類的新實例,同時指定可同時授予的請求的初始數量和最大數量。

示例

我們改造一下前面 Semaphore 中的示例:

    class Program
    {
        // 求和
        private static int sum = 0;
        private static SemaphoreSlim _pool;

        // 判斷十個線程是否結束瞭。
        private static int isComplete = 0;
        static void Main(string[] args)
        {
            Console.WriteLine("執行程序");

            // 設置允許最大三個線程進入資源池
            // 一開始設置為0,就是初始化時允許幾個線程進入
            // 這裡設置為0,後面按下按鍵時,可以放通三個線程
            _pool = new SemaphoreSlim(0, 3);
            for (int i = 0; i < 10; i++)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(AddOne));
                thread.Start(i + 1);
            }

            Console.WriteLine("任意按下鍵(不要按關機鍵),可以打開資源池");
            Console.ReadKey();
            // 
            _pool.Release(3);

            // 這裡沒有任何意義,就單純為瞭演示查看結果。
            // 等待所有線程完成任務
            while (true)
            {
                if (isComplete >= 10)
                    break;
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
            Console.WriteLine("sum = " + sum);
            // 釋放池
        }

        public static void AddOne(object n)
        {
            Console.WriteLine($"    線程{(int)n}啟動,進入隊列");
            // 進入隊列等待
            _pool.Wait();
            Console.WriteLine($"第{(int)n}個線程進入資源池");
            // 進入資源池
            for (int i = 0; i < 10; i++)
            {
                Interlocked.Add(ref sum, 1);
                Thread.Sleep(TimeSpan.FromMilliseconds(200));
            }
            // 解除占用的資源池
            _pool.Release();
            isComplete += 1;
            Console.WriteLine($"                     第{(int)n}個線程退出資源池");
        }
    }

SemaphoreSlim 不需要 Close()

兩者在代碼上的區別是就這麼簡單。

區別

如果使用下面的構造函數實例化 Semaphor(參數name不能為空),那麼創建的對象在整個操作系統內都有效。

public Semaphore (int initialCount, int maximumCount, string name);

Semaphorslim 則隻在進程內內有效。

SemaphoreSlim 類不會對 WaitWaitAsync 和 Release 方法的調用強制執行線程或任務標識。

而 Semaphor 類,會對此進行嚴格監控,如果對應調用數量不一致,會出現異常。

此外,如果使用 SemaphoreSlim(Int32 maximumCount) 構造函數來實例化 SemaphoreSlim 對象,獲取其 CurrentCount 屬性,其值可能會大於 maximumCount。 編程人員應負責確保調用一個 Wait 或 WaitAsync 方法,便調用一個 Release。

這就好像筆筒裡面的筆,沒有監控,使用這使用完畢後,都應該將筆放進去。如果原先有10支筆,每次使用不放進去,或者將別的地方的筆放進去,那麼最後數量就不是10瞭。

到此這篇關於C#多線程系列之資源池限制的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: