執行緒同步介紹及 生產者消費者問題舉例 C#版

HelloLLLLL 發表於 2022-01-23
C#
現在有五個工人在果園摘水果,一次只能摘一個,摘下的水果放入一個框中,這種框最多隻能裝50個橘子,一共有兩個這樣的框。當一個工人裝框時,其他工人不能在使用這個框。當兩個框都裝滿了,工人只有等框裡有剩餘位置後,才能在摘果子。然後,有四個小孩來框裡拿橘子,一次最多拿一個,並且當一個小孩在框前拿橘子時,其他小孩只能到另一個框拿橘子,如果兩個框前都有小孩拿橘子,那麼剩餘要拿橘子的小孩只能等待。(這個題目是我自己編的,可能不是很準確)
現在要設計一個程式模擬這樣一個過程。
 
分析:框是互斥資源,每次放橘子前 得判斷有沒有空閒得框,有就佔住加鎖,然後裡面執行放橘子得方法。放完之後再解鎖。框可以用佇列表示。
工人和小孩可以用Task模擬。
 
這裡需要兩種鎖,一種是放橘子得時候得一把Monitor鎖,一種是當沒有空閒得框後,加的AutoResetEvent鎖。
當使用兩把鎖得時候,需要特別小心,稍不注意都會引發死鎖。
 
Monitor鎖再使用得時候,得用引用變數作為加鎖得物件,不要用字串和值變數。雖然再用值變數時,編譯器不會報錯,但是執行時,Enter會裝箱,把值變數變為引用變數,但是再Exit時,依然是個值變數,這樣Enter和Exit的鎖變數就不是同一個變數,造成找不到鎖的情況,就會丟擲異常。
 
另外使用Monitor枷鎖時,應該使用 try{}finally{}語句塊,保證總是會被解鎖,否則遇到異常,不執行解鎖語句,就死鎖了。
其實lock語句塊就是Monitor的簡便方法,內部使用的還是Monitor。
 
對於AutoResetEvent而言,可以暫停和喚醒執行緒,再不同執行緒可以相互喚醒和阻塞。這樣就非常的靈活。其實推薦使用ManualResetEvent,因為比起AutoResetEvent,可以喚起多個執行緒,如果說小孩一次拿多個橘子,這種方式就比AutoResetEvent有優勢,因AutoResetEvent只喚醒一個執行緒。
 
執行緒的同步還有其他方法,比如再數值上同步 有InterLock,其他的如訊號量(Sema'phore)同步,CountDownEvent。
同步的應用,還可以是用程式間同步的方法,實現在一臺主機上,每次只能啟動一個相同的應用程式,這時可以使用Mutex。
 
避免資源線上程同步中互斥,還可以用 執行緒本地儲存技術,ThreadLocal,例子:
0
詳見:《C#本質論》第三版,第19章
下面直接看程式碼:
 internal class Program
    {
        //最多容納50個橘子每個框
       static  readonly int MAX = 50;
        //兩個框
        static List<ConcurrentQueue<Orange>> Queues = 
            new List<ConcurrentQueue<Orange>>();
        //記錄空閒的框
        static List<int> QidxBags = new List<int>();

        static int MaxO = 1000;     //最多摘1000個橘子

        static readonly object Sync = new object();
        static readonly object Sync2 = new object();
        //比起AutoResetEvent,可以喚起多個執行緒,如果說小孩一次拿多個橘子,而不是一個,
        //這種方式比AutoResetEvent有優勢,因為AutoResetEvent只喚醒一個執行緒。
        static ManualResetEvent MResetEvent = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            Queues.Add(new ConcurrentQueue<Orange>());
            Queues.Add(new ConcurrentQueue<Orange>());
           
            for (int i = 0; i < Queues.Count; i++)
            {
                QidxBags.Add(i);
            }
            TaskProduceAndComsummer();
            Console.ReadKey();
        }


        static int GetQueuesIdx()
        {
            int idx = -1;

            int count = QidxBags.Count;

            if (count > 0)
            {
                return count;
            }


            return idx;
        }

        static bool IsEmpty()
        {
            foreach (var item in Queues)
            {
                if (item.Count >0)
                {
                    return false;
                }
            }
            return true;
        }

        static bool IsFull()
        {
            foreach (var item in Queues)
            {
                if (item.Count < MAX)
                {
                    return false;
                }
            }
            return true;
        }

        static void TaskProduceAndComsummer()
        {
            for (int i = 0; i < 5; i++)
            {
                string name = "工人_" + (i + 1);
                Task t = new Task(Produce, (object)(name));
                
                t.Start();
            }

            for (int i = 0; i < 3; i++)
            {
                string name = "小孩_" + (i + 1);
                Task t = new Task(Consumer, (object)(name));

                t.Start();
            }



        }
        static void Produce(object name)
        {
            while (true&&MaxO>0)
            {
                int count = -1;
                int iPos = -1;
                lock (Sync2)
                {
                    count = GetQueuesIdx();
                }
                if (count > 0&&!IsFull())
                {
                    bool refTaken = false;

                    Monitor.Enter(Sync, ref refTaken);
                    bool isPut = false;
                    try
                    {
                       
                        for (int i = 0; i < count; i++)
                        {

                            iPos = QidxBags[i];
                            var q = Queues[iPos];

                            if (q.Count < MAX)
                            {
                                QidxBags.Remove(iPos);
                                q.Enqueue(Orange.GetOrange());
                                MaxO -= 1;
                                Console.WriteLine(name + ":+摘了一個橘子,放入框【" + iPos + "】中");
                                Console.WriteLine("框一數量:{0},框二數量{1}", Queues[0].Count, Queues[1].Count);
                                isPut = true;
                                //喚醒小孩執行緒
                                MResetEvent.Set();
                                break;
                            }

                        }
                    }
                    finally
                    {
                        if (refTaken)
                        {
                            if (iPos > -1)
                            {
                                QidxBags.Add(iPos);
                            }

                            Monitor.Exit(Sync);
                            if (!isPut)
                            {
                                Console.WriteLine("滿了");
                            }
                           
                        }
                    }
                }
                else
                {
                    MResetEvent.WaitOne();
                }
            
            
            }
        }

        static void Consumer(object name)
        {
            while (true)
            {
                int count = GetQueuesIdx();
                int iPos = -1;
                if (count > 0&&!IsEmpty())
                {
                    bool refTaken = false;
                    bool isPut = false;
                    Monitor.Enter(Sync, ref refTaken);
                    try
                    {
                        for (int i = 0; i < count; i++)
                        {

                            iPos = QidxBags[i];
                            var q = Queues[iPos];

                            if (q.Count >0)
                            {
                                QidxBags.Remove(iPos);
                                Orange o = null;
                                q.TryDequeue(out o);
                                Console.WriteLine(name + ":+拿了一個橘子,從框【" + iPos + "】中");
                                Console.WriteLine("框一數量:{0},框二數量{1}", Queues[0].Count, Queues[1].Count);
                                isPut = true;
                                //框有容量了,可以放了,所以喚醒被阻塞得工人執行緒
                                MResetEvent.Set();
                                break;
                            }

                        }
                    }
                    finally
                    {
                        if (refTaken)
                        {
                            if (iPos > -1)
                            {
                                QidxBags.Add(iPos);
                            }
                            Monitor.Exit(Sync);
                            if (!isPut)
                            {

                                Console.WriteLine("都空了");
                              
                            }
                            
                        }
                    }
                }
                else
                {
                    MResetEvent.WaitOne();//阻塞
                }
            }
        }
    }

    public class Orange
    {
        public static Orange GetOrange()
        {
            Random rand = new Random();
            int t = rand.Next(10, 20);
            Thread.Sleep(t);
            return new Orange();
        }
    }

部分結果:

執行緒同步介紹及 生產者消費者問題舉例 C#版