進擊谷歌:多執行緒下程式順序怎麼穩定不亂?

架構師修行之路發表於2020-04-26

題目

比如我們有三個方法,類似以下程式碼

 static void Methond1()
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} 執行 第一個方法==");
        }
        static void Methond2()
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} 執行 第二個方法=======");
        }
        static void Methond3()
        {
           Console.WriteLine($"{Thread.CurrentThread.Name} 執行  第三個方法=======");
        }
    }

分別初始化三個執行緒去執行這三個方法,每個執行緒執行的方法可以任意指定,但是方法的執行順序必須是:Methond1=》Methond2=》Methond3

示例:

輸入:3,2,1  
第一個執行緒執行方法3,第二個執行緒執行方法2,第三個執行緒執行方法1
輸出:Methond1的輸出=》Methond2的輸出=》Methond3 的輸出 ,即:
xxx執行 第一個方法==
xxx執行 第二個方法==
xxx執行 第三個方法==

題解分析

這道題本質上考的是多執行緒環境資源競爭的問題,在多執行緒環境下要想以固定某個順序執行方法,必須要有一個類似屏障的東西來阻止不該執行的執行緒,或者換句話說必須要有一個機制來順序化多個執行緒。這也是我們平時多執行緒開發環境中必須要面對的問題,現代伺服器的cpu都是多核心,我們平時用的最多的java,c#等高階語言是能夠利用伺服器多核心這個優勢的,這裡是一個程式內,多執行緒就可以利用多核心。當然也有一些別的技術,比如Nodejs,雖然執行是單執行緒的,但是可以利用多程式來提高多物理cpu的利用率。

說的有點遠了,接下來還是看這道題,這道題目考驗的是多執行緒下資源的競爭問題和執行緒間的通訊問題,說的直白一點就是:Methond1未執行之前,任何方法都不能執行(這裡是指方法內的程式碼),Methond1執行完之後,會通知執行Methond2的執行緒,Methond2開始執行,Methond2執行完畢會通知執行Methond3的執行緒,以此類推。所以這道題的關鍵在於阻塞某個執行緒執行和通知某個執行緒執行。

在很多語言中都提供了lock的機制,或者訊號量的機制。這些都是可以實現執行緒間通訊的的解決方案,當然最簡單暴力的方式還是通過共享變數的方式來實現通知,雖然有很多缺陷,但是在很多情況下確實是最簡單快捷的方案。下邊就以c#語言為例:

 class Program
    {
        //把三個方法分別對應到 委託,為了方便執行方法
        static Action FirstAction = Methond1;
        static Action SecondAction = Methond2;
        static Action ThirdAction = Methond3;

        static Dictionary<int, Action> ActionDic = new Dictionary<int, Action>() { { 1, FirstAction }, { 2, SecondAction }, { 3, ThirdAction } };
        static void Main(string[] args)
        {
            while (true)
            {
                Console.WriteLine("請輸入順序");
                var inputStr = Console.ReadLine();
                IsFinish1 = IsFinish2 = false;

                var input = inputStr.Split(",").Select(s => int.Parse(s));
                foreach (var i in input)
                {
                    var method = ActionDic.GetValueOrDefault(i);
                    Thread t = new Thread(method.Invoke);
                    t.Name = "執行緒" + i;

                    Console.WriteLine($"{t.Name} 執行方法 {method.Method.Name}");
                    t.Start();
                }
            }
        }

        static object objLock = new object();
        static bool IsFinish1 = false; //第一方法是否執行完畢
        static bool IsFinish2 = false; //第二方法是否執行完畢


        static void Methond1()
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} 執行 第一個方法==");
            IsFinish1 = true;

        }
        static void Methond2()
        {
            while (true)
            {
                if (IsFinish1)
                {
                    Console.WriteLine($"{Thread.CurrentThread.Name} 執行 第二個方法=======");
                    IsFinish2 = true;
                    break;
                }
            }
        }
        static void Methond3()
        {
            while (true)
            {
                if (IsFinish2)
                {
                    Console.WriteLine($"{Thread.CurrentThread.Name} 執行  第三個方法=======");
                    break;
                }
            }
        }
    }

測試執行:

請輸入順序
2,3,1
執行緒2 執行方法 Methond2
執行緒3 執行方法 Methond3
執行緒1 執行方法 Methond1
請輸入順序
執行緒1 執行 第一個方法==
執行緒2 執行 第二個方法=======
執行緒3 執行  第三個方法=======
3,2,1
執行緒3 執行方法 Methond3
執行緒2 執行方法 Methond2
執行緒1 執行方法 Methond1
請輸入順序
執行緒1 執行 第一個方法==
執行緒2 執行 第二個方法=======
執行緒3 執行  第三個方法=======

不要太糾結以上的程式碼,以上只是作為拋磚引玉的作用,就算是有執行錯誤,思路我覺得還是沒有問題的,當然確實有很多要優化的地方,比如等待執行的執行緒一直在耗費cpu資源的情況,可以替換為自旋鎖或者混合鎖,有興趣的同學可以進行優化一番。

另闢蹊徑

但是,作為語法很優秀的c#怎麼能就此止步呢?c#本身提供了Task的機制,可以把它看做是類似golang的協程(雖然不是很像)。基於task的機制我們很容易就完成,task的 ContinueWith機制就是為此而生的,它可以很方便的把多個並行的task序列化,有興趣的同學可以試一下,但是我要說的並非ContinueWith。既然要執行的順序化,如果我把執行的程式碼放在有順序的容器中,然後按照順序去執行,豈不快哉?不多廢話:

class TaskTest
    {
        static Func<Task> FirstAction = Methond1;
        static Func<Task> SecondAction = Methond2;
        static Func<Task> ThirdAction = Methond3;
        static Dictionary<int, Func<Task>> ActionDic = new Dictionary<int, Func<Task>>() { { 1, FirstAction }, { 2, SecondAction }, { 3, ThirdAction } };
        static Func<Task>[] tList = new Func<Task>[3];
       public static void Start()
        {
            while (true)
            {
                Console.WriteLine("請輸入順序");
                var inputStr = Console.ReadLine();
             
                var input = inputStr.Split(",").Select(s => int.Parse(s));
                foreach (var i in input)
                {
                    var method = ActionDic.GetValueOrDefault(i);
                    tList[i-1] = method;
                }
                foreach (var t in tList)
                {
                    t.Invoke().Wait();
                }
            }
        }

        static Task Methond1()
        {
           return Task.Run(()=> {
                Console.WriteLine($"{Thread.CurrentThread.Name} 執行 第一個方法==");
            });
        }
        static Task Methond2()
        {
           return  Task.Run(() => {
                Console.WriteLine($"{Thread.CurrentThread.Name} 執行 第二個方法=======");
            });
        }
        static Task Methond3()
        {
            return Task.Run(() => {
                Console.WriteLine($"{Thread.CurrentThread.Name} 執行  第三個方法=======");
            });
        }
    }

測試:

請輸入順序
2,3,1
 執行 第一個方法==
 執行 第二個方法=======
 執行  第三個方法=======
請輸入順序
3,2,1
 執行 第一個方法==
 執行 第二個方法=======
 執行  第三個方法=======
請輸入順序
1,3,2
 執行 第一個方法==
 執行 第二個方法=======
 執行  第三個方法=======
請輸入順序

是不是很爽,毫不猶豫的說c#是一門很優秀的語言,在併發方面做的很優秀。以上Task的程式碼並非是最優程式碼,比如 可以用Await來代替 wait(),這樣的話在真實環境中可以釋放當前執行緒來提高吞吐量,記住:await方式提高的吞吐量,並非縮短了單次請求的執行時間。

相關文章