首頁 > 軟體

C#多執行緒系列之資源池限制

2022-02-13 19:00:45

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#多執行緒系列之資源池限制的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com