顯示具有 Synchronization 標籤的文章。 顯示所有文章
顯示具有 Synchronization 標籤的文章。 顯示所有文章

2022年7月20日 星期三

C# : 在進行多執行緒程式設計下, 若存取 List 共用資源,是否會具有執行緒安全的特性呢?

在進行多執行緒程式設計下, 若存取 List 共用資源,是否會具有執行緒安全的特性呢?

執行緒安全這個議題,在進行多執行緒程式設計下,是個相當棘手與難處理的問題,很多情況下,是來自於開發者人類本身的思維,畢竟,我們人的思考並不是以平行方式來評量的,很多人同一個時間,畢竟僅能夠做一件事情。

這篇文章中,將要來說明一個多執行緒程式設計下常遇到的問題,那就是很多執行緒當要存取共用資源物件的時候,在這裡使用的靜態物件,該共用存取物件不具備 [執行緒安全 Thread Safety] 特性,就會造成執行結果是無法預期的,也就是說,每次執行結果都是不同的。

底下的範例,將會設計共用存取資源為 List

namespace Multiple_Thread_Using_List_Type_Is_Thread_Safety
{
    internal class Program
    {
        static int MaxCount = 2000;
        static int ThreadCount = Environment.ProcessorCount;
        static List<int> ints = new List<int>();
        static List<AutoResetEvent> events = new List<AutoResetEvent>();
        static void Main(string[] args)
        {
            for (int i = 0; i < Environment.ProcessorCount; i++)
            {
                AutoResetEvent autoResetEvent = new AutoResetEvent(false);
                events.Add(autoResetEvent);
                ThreadPool.QueueUserWorkItem(_ =>
                {
                    for (int i = 1; i <= MaxCount; i++)
                    {
                        ints.Add(i);
                    }
                    autoResetEvent.Set();
                });
            }

            WaitHandle.WaitAll(events.ToArray());

            var groupInts = ints.GroupBy(x => x)
                 .Select(x => new { Number = x.Key, Count = x.Count() })
                 .OrderBy(x => x.Count);

            //foreach (var item in groupInts)
            //{
            //    Console.WriteLine($"{item.Number} = {item.Count}");
            //}

            var groupByCount = groupInts
                .GroupBy(x => x.Count)
                .Select(x => new { Count = x.Key, Times = x.Count() })
                .OrderBy(x => x.Count);

            foreach (var item in groupByCount)
            {
                Console.WriteLine($"{item.Count} = {item.Times}");
            }
        }
    }
}

首先,將會透過執行緒集區取得這台電腦邏輯處理器數量的執行緒,在這些在這些執行緒內,都做同一件事情,那就是進行一個 2000 次的迴圈,並且把當時迴圈內的索引值,加入到這個 List<int> 靜態共用資源內,最後將會進行統計出,同樣的數值整數出現過幾次,理論上(在我的電腦上,邏輯處理器的數量為 8),應該要看到 1~2000 這些數值,在 List<int> 這個集合物件內,每個數值都會看到有 8 次出現。

幻想是美麗的,現實是殘酷的,底下是這段程式碼的執行結果

多執行幾次,將會看到類似底下的不同結果顯示在螢幕上

1 = 1
2 = 6
3 = 64
4 = 396
5 = 862
6 = 672
2 = 32
3 = 332
4 = 426
5 = 719
6 = 293
7 = 189
8 = 9
13 = 1

身為程式開發者應該都是相當的聰明的,你可以想像到發生了什麼問題嗎?

沒錯,因為在多執行緒的環境下,造成了沒有執行緒安全現象,導致這個程式的執行結果是不可預期的

首先,在 List 文件中可以看到底下這句話

執行緒安全性
此類型Visual Basic) 成員中的公用靜態 (Shared 是安全線程。 並非所有的執行個體成員都是安全執行緒。

在 上 List<T> 執行多個讀取作業是安全的,但如果正在讀取集合時修改集合,可能會發生問題。 為了確保執行緒安全,請在讀取或寫入作業期間鎖定集合。 若要讓多個執行緒存取集合以進行讀取和寫入,您必須實作自己的同步處理。 如需具有內建同步處理的集合,請參閱 命名空間中的 System.Collections.Concurrent 類別。 如需原本就安全線程的替代方法,請參閱 類別 ImmutableList<T> 。

很清楚的看出,因為這裡的共用資源(每個執行緒都要存取的物件)型別為 List<T> ,若產生這個型別出來的物件,並不是每個成員都是執行緒安全的,所以,使用這個型別的執行個體在多執行緒環境下來運行,當然會造成每次的執行結果是無法預期的。

提示

會發生不具有執行緒安全情況,是因為多執行緒彼此間發生了競賽條件所產生的,這是在計算機運行中一定會發生的,因此,需要特別進行設計,避免這些問題。

在非常渺茫的機會中,可能會看到期望中的執行結果,若你可以執行出來這樣的結果,那就表示你現在的手氣相當的好

8 = 2000

在執行過程中,多執行幾次,將會有遇到這樣的例外異常的機會

Unhandled exception. System.ArgumentOutOfRangeException: capacity was less than the current size. (Parameter 'value')
   at System.Collections.Generic.List`1.set_Capacity(Int32 value)
   at System.Collections.Generic.List`1.Grow(Int32 capacity)
   at System.Collections.Generic.List`1.AddWithResize(T item)
   at Multiple_Thread_Using_List_Type_Is_Thread_Safety.Program.<>c__DisplayClass4_0.<Main>b__6(Object _) in C:\Vulcan\Github\CSharp2022\Multiple-Thread-Using-List-Type-Is-Thread-Safety\Multiple-Thread-Using-List-Type-Is-Thread-Safety\Program.cs:line 19
   at System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute()
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
   at System.Threading.Thread.StartCallback()

看到上述的說明,強烈建議開發者,若要進行多執行緒程式設計,要存取共用資源,選擇具有執行緒安全的型別;對於 .NET API 文件或者其他第三方套件,也要特別來查看,對於靜態成員或者執行個體成員,是否具有執行緒安全的特性,若沒有,就要考慮使用 同步處理原始物件概觀 其中一種機制來保護共用資源的存取行為。

針對上面的程式碼,想要完成一個具有執行緒安全的程式碼,選擇一個提供執行緒安全的類別與API,在這裡將會把原先的 static List<int> ints = new List<int>(); 敘述,修改成為這樣的宣告 static ConcurrentBag<int> ints = new ConcurrentBag<int>(); ,而其他的程式碼都不用修正,就完成了

這裡使用的 ConcurrentBag Class 這個類別,這裡是官方的說明:代表安全執行緒的未排序物件集合

其中對於執行緒安全的介紹如下

執行緒安全性

所有公用和受保護的成員 ConcurrentBag 都是安全線程,而且可以從多個執行緒同時使用。 不過,透過實作的其中一個介面 ConcurrentBag 存取成員,包括擴充方法,不保證為安全線程,而且可能必須由呼叫端同步處理。




2018年8月25日 星期六

在 .NET 使用 執行緒的同步 Synchronization - ManualResetEvent vs AutoResetEvent

在 .NET 使用 執行緒的同步 Synchronization - ManualResetEvent vs AutoResetEvent

當您在開發多執行應用程式的時候,將會遇到這樣的需求,要能夠在不同的執行緒執行過程中,進行互相的協調、以便同步進行後續的處理工作。在這裡,我們將模擬一個實際的情境,例如,在賽馬場中,將會有五組參賽人員要進行比賽,而要比賽之前,大家需要依序進入到起跑點內,這樣,裁判才能夠鳴槍,讓大家一同起跑;另外,我們還需要能夠下達一個指定,讓這個程式可以結束執行。
這篇文章的範例程式碼,可以從 https://github.com/vulcanlee/CSharpNotes2018/tree/master/ManualResetEventDemo 取得
面對這樣的需求,我們將會需要用到7個執行緒(其中,有6個背景執行緒,1個前景執行緒)來完成這樣的情境
  • 每個參賽人員的執行緒
    因為會有五組人員要參加比賽,這裡共會產生五個執行緒,在此,使用 ThreadPool.QueueUserWorkItem 產生5個執行緒,並且可以看出,這五個執行緒都是背景執行緒,也就是說,若主執行緒執行完成之後,不論這五個執行緒是否有執行完成,該處理程序也會結束執行的。
    這個執行緒的委派方法會模擬要進行準備工作,因此,當參賽人員之比賽執行緒開始執行的時候,會模擬等候 2~5 秒鐘的時間。
    接著,會使用 WaitForGameStart.WaitOne(); 方法,等候裁判通知比賽要開始 (在裁判端的執行緒中,會透過 WaitForGameStart.Set(); 方法,通知這五個執行緒比賽正式開始);當執行緒執行WaitForGameStart.WaitOne(); 方法 的時候,該執行將會在 封鎖 (Block) 狀態下,也就是,這個執行緒無法執行任何程式碼。
    然後,在比賽執行緒將會模擬進行比賽,在這裡比賽的執行緒中,將會模擬休息 5~10 秒鐘,最後,比賽用的執行緒將會結束執行。
  • 裁判的執行緒
    我們將會透過 ThreadPool.QueueUserWorkItem 產生一個背景執行緒,作為裁判下達通知與進行相關動作的執行程式,在這個執行緒中,會使用 ConsoleKeyInfo key = Console.ReadKey(); 等候裁判下達指示,若輸入 B 按鍵,便會下達 WaitForGameStart.Set(); 方法,讓參賽的五個執行緒開始同時來進行比賽;若下達 Q 按鍵,則會執行 WaitForExitProgram.Set(); 方法,此時,在主執行緒中的最後一行敘述,將不會被封鎖 Block 住,而會繼續執行;因為主執行緒為前景執行緒,當前景執行緒執行完畢後,不論背景執行緒是否有執行完成,整個處理程序將會結束運行。
這個螢幕截圖,將會是該範例程式的執行結果。
ManualResetEvent AutoResetEvent
底下是上述說明的測試程式碼,若想要了解 AutoResetEvent 與 ManualResetEvent 這兩個類別的差異,可以試著將 WaitForGameStart 變數的型別修改成為 AutoResetEvent,體驗一下執行結果有何差異。
不論是 AutoResetEvent 與 ManualResetEvent 都是提供執行緒間的同步處理工作,該物件內都有初始狀態設定,在我們這個範例中,設定為 fasle,表示尚未收到其他執行緒的為已收到訊號通知,因此,當該執行緒執行到 WaitForGameStart.WaitOne(); 敘述的值後,該執行緒將會被凍結,封鎖 Block 在行程式碼上。
此時,您可以想像有個閘門,只要您下令將閘門打開 (在其他執行緒上執行 WaitForGameStart.Set(); 方法),這些被封鎖的執行緒將會自動執行;而 AutoResetEvent 與 ManualResetEvent 的差異在於,對於前者若在其他執行緒上執行了 WaitForGameStart.Set(); 敘述,該閘門會開啟,但是,只允許一個人通過,並且就會立即關閉起來;而後者,則是打開之後,所有的執行緒都會繼續執行下去,除非我們下達關閉閘門指令。
您可以試著修改 static ManualResetEvent WaitForGameStart = new ManualResetEvent(false); 為 static AutoResetEvent WaitForGameStart = new AutoResetEvent(false),接著執行看看差異在哪裡。
C Sharp / C#
class Program
{
    // 等候通知便結束程式執行
    static AutoResetEvent WaitForExitProgram = new AutoResetEvent(false);
    // 等候通知,便開始進行比賽
    static ManualResetEvent WaitForGameStart = new ManualResetEvent(false);

    // 定義隨機亂數用於測試之用
    static Random 隨機亂數 = new Random();

    static void Main()
    {
        // 使用 ThreadPool.QueueUserWorkItem 產生5個執行緒
        Console.WriteLine("開始比賽前的比賽準備");
        for (int i = 1; i <= 5; i++)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(進行比賽), i);
        }
        Console.WriteLine("等候裁判通知,比賽就會開始");


        ThreadPool.QueueUserWorkItem((x) =>
        {
            while (true)
            {
                ConsoleKeyInfo key = Console.ReadKey();
                if (key.Key == ConsoleKey.B)
                {
                    WaitForGameStart.Set();
                }
                if (key.Key == ConsoleKey.Q)
                {
                    WaitForExitProgram.Set();
                }
            }
        });

        Console.WriteLine("等候通知,該程式就會結束執行");
        WaitForExitProgram.WaitOne();
    }

    static void 進行比賽(Object state)
    {
        // 向等候的執行緒通知發生事件
        int are = (int)state;
        int time = 1000 * 隨機亂數.Next(2, 5);
        Console.WriteLine($"參賽者 {are} 需要 {time} 毫秒的時間來準備");
        Thread.Sleep(time);
        Console.WriteLine($"參賽者 {are} 準備好了");
        // 設定作業已經成功
        WaitForGameStart.WaitOne();
        Console.WriteLine($"參賽者 {are} 開始進行比賽");
        time = 1000 * 隨機亂數.Next(5, 10);
        Thread.Sleep(time);
        Console.WriteLine($"參賽者 {are} 抵達終點");
    }
}

關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程