流水線-提升效率
聰明的小明同學,昨天聽了外老師的講解之後,意猶未盡的回家,正琢磨著,怎麼樣使用多執行緒技術,加速大量小任務的處理速度!
就在今天早晨,小明在早餐店看到了這樣一幕:
這是一家非常火爆的早餐店,老闆配備了兩臺收銀機,在早高峰的時候,兩臺收銀機全速執行,以提高顧客點單的速度。
小明點了一份熱乾麵,只見收銀員將小明的訂單遞給了第一位小哥,小哥接到訂單之後,非常熟練地將適量麵條,放入一個錐形的容器中,然後放入身旁的熱鍋中去煮。
這時候,前面的另一個阿姨,正好取走了放在鍋裡的另一份麵條,快速的取一個碗,倒入麵條,加入芝麻醬,還有其他調料,半分鐘左右,這一份熱面就交到了小明前面的一位小姐姐手中。
然後阿姨就開始做小明的那一碗麵條了。。。
小明似乎明白了什麼,他興致勃勃的來到教室,向我講述他買早餐時的所見,說道:
老師,流水線啊!我們可以讓一個執行緒,處理多個小任務,不就可以避免開啟過多執行緒的問題了嗎!這樣在提高任務處理速度的同時,也避免了開啟過多執行緒的昂貴成本。如果我們有多種任務,還可以開啟多個執行緒還處理不同的任務。
這時外老師的臉上,洋溢著滿意的笑容,對小明說到:小明,你前途無量啊!加油,好好學。
觸類旁通
流水線早餐店的老闆,不可能為每一個顧客,僱一個員工為其提供服務。而是僱幾個員工,分別負責不同的工序。在比較耗時的工序,通常會增加員工的數量,以保證流水線中的每一個員工都不閒著。
流水線同時還有一大好處,就是每一個員工,只需掌握自己這一個工序的技能,每天重複同樣的工作,動作非常的嫻熟。員工培養的成本也非常低。還不用擔心員工自己出去單幹。。。
ThreadPool這個思路,在軟體行業,也同樣流行。
我們可以自己將任務分類,分組,將每一組任務,交給一個執行緒來處理。我們只需要簡單的程式碼,就可以實現這個邏輯。
當然,軟體行業還有一個特點,就是通常不需要我們自己造輪子,而是直接使用別人造好的輪子。沒錯,微軟已經為我們提供了現成的執行緒池實現ThreadPool。我們直接使用即可。
下面進入例項講解,先回顧一下上一篇文章的示例:
class Data{ public Data(int id) { ID = id; } public int ID { get; set; } public void DoSomeThing() { ID += 1; }}static void MultiThread(IList<Data> datas){ foreach (var item in datas) { var thd = new System.Threading.Thread( () => item.DoSomeThing()); thd.Start(); // 這裡先不管 thd.Join(); // 也不要求 DoSomeThing 始終執行 }}
然後我們使用ThreadPool再寫一個測試函式:
static void MultiThreadPool(IList<Data> datas){ foreach (var item in datas) { ThreadPool.QueueUserWorkItem( obj => item.DoSomeThing()); // 我們無法控制 ThreadPool 中的執行緒的 開始、掛起、和中止等 // 這個需要注意的地方 }}
我們在兩個測試函式中,都只將任務新增到多執行緒中去執行,先不考慮其執行結果,單純比較執行緒的新增效率:
public static void Main(){ Console.WriteLine("Hello Thread World!"); var dts = MakeData(); var sw = new Stopwatch(); sw.Start(); MultiThread(dts); sw.Stop(); var tc1 = sw.ElapsedTicks; Console.WriteLine($"Multi Thread Time Costs: {tc1}"); sw.Restart(); MultiThreadPool(dts); sw.Stop(); var tc2 = sw.ElapsedTicks; Console.WriteLine($"Thread Pool Time Costs: {tc2}"); Console.WriteLine($"Rate: {tc1/tc2}"); // 加入對比 Console.ReadKey();}
編譯執行結果如下:
執行結果
可以看到,ThreadPool的效率,提升了5倍左右。這可能不算特別突出的效率提升。但是當我們把任務的數量提升到1萬個,情況會有什麼不同嗎:
static IList<Data> MakeData(){ var dataNum = 10000; // 任務數量 var datas = new List<Data>(dataNum); for (int i = 0; i < dataNum; i++) { datas.Add(new Data(i)); } return datas;}
執行結果如下:
1萬個任務結果
可以看到,速度提升已經到了200多倍了。可見任務數量越多,ThreadPool的優勢越明顯。
最後我們將任務數量調整為20個,並在程式碼中,加入檢測執行緒ID的程式碼,然後觀察一下執行緒池中的任務的執行情況:
static IList<Data> MakeData(){ var dataNum = 20; // 任務數量 var datas = new List<Data>(dataNum); for (int i = 0; i < dataNum; i++) { datas.Add(new Data(i)); } return datas;}class Data{ public Data(int id) { ID = id; } public int ID { get; set; } public void DoSomeThing() { ID += 1; Console.WriteLine( $"Thread ID:{Thread.CurrentThread.ManagedThreadId}" + $"\t Data ID:{ID}"); }}public static void Main(){ Console.WriteLine("Hello Thread World!"); var dts = MakeData(); MultiThreadPool(dts); Console.ReadKey();}
執行結果如下:
執行緒ID
可以看出,20個任務,分佈在8個執行緒中執行,其中10號執行緒,執行的任務數量最多。
由此可以看出來,ThreadPool確實可以重複使用執行緒。其中的執行緒,和流水線中的工人一樣,可以重複做一個工序中的工作。
當然,ThreadPool中的執行緒,比工人厲害的地方,就是執行緒可以完成各種各樣的工序,哪個工序需要,就去哪個環節幹活。
所以使用ThreadPool,比現實中的流水線,可以更加靈活,因為每一個工人都是全能選手。
使用ThreadPool注意事項我們無法控制ThreadPool中的執行緒的開始、掛起、和中止。
長時間執行線上程,不宜放在ThreadPool中,而是應該單獨開啟執行緒,否則會導致執行緒池被長期佔用,而無法執行其他執行緒。
本文中的示例,可能並不完全合適。大家在實際的工作中,要學會舉一反三,觸類旁通,靈活運用。
終於講完了!我看了一眼小明,他又在看著我點頭!
小明同學
踩坑記錄暫無
下期預告下面是給同學們準備的乾貨,正在陸續發貨中哦:
多個執行緒之間的資源共享問題
多執行緒死鎖問題
Task
Parallel
await/async
Linq與PLinq (ParallelEnumerable)
。。。 。。。