買櫝還珠
我們說到,多執行緒,可以利用CPU的多個核心,並行執行,從而提升程式的效率。
有個聰明的同學,小明,向我問到:那是不是執行緒開得越多,程式執行就越快呢?
為了搞明白這個問題,外老師專門寫了個測試程式,用來掰扯掰扯多執行緒的代價那點事。
例項演練先來一個Data類,模擬某種資料:
class Data{ public Data(int id) { ID = id; } public int ID { get; set; } public void DoSomeThing() { ID += 1; }}
Data.DoSomeThing模擬某種操作,這裡簡單在做一個加法。
然後我們生成一批測試資料:
static IList<Data> MakeData(){ var dataNum = 100; var datas = new List<Data>(dataNum); for (int i = 0; i < dataNum; i++) { datas.Add(new Data(i)); } return datas;}
下面用單執行緒呼叫全部資料的DoSomeThing方法,來模擬批次資料處理:
static void SingleThread(IList<Data> datas){ foreach (var item in datas) { item.DoSomeThing(); }}
下面是一個不正確的多執行緒模擬批次資料處理的函式:
static void MultiThread(IList<Data> datas){ foreach (var item in datas) { var thd = new System.Threading.Thread( () => item.DoSomeThing()); thd.Start(); // 這裡先不管 thd.Join(); // 也不要求 DoSomeThing 始終執行 }}
然後編寫測試程式,對以上兩個函式進行執行速度比較:
static void Main(string[] args){ Console.WriteLine("Hello Thread World!"); var dts = MakeData(); var sw = new Stopwatch(); sw.Start(); SingleThread(dts); sw.Stop(); Console.WriteLine($"Single Thread Time Costs: {sw.ElapsedTicks}"); sw.Restart(); MultiThread(dts); sw.Stop(); Console.WriteLine($"Multi Thread Time Costs: {sw.ElapsedTicks}"); Console.ReadKey();}
由於我們的資料量不大,我們使用CPU的ticks數量,來計時(要不然看不出來區別),相信有經驗的同學,已經知曉執行結果了:
多執行緒反而更慢
可以看到,多執行緒執行消耗的時間,是單執行緒的200倍!而且我們還沒有等待所有執行緒執行完畢。是不是讓人大跌眼鏡!
小明看到這裡,心裡已經有了初步的答案了。但是外老師還是從他滿臉的問號當中,看出了他心中的疑惑:這是什麼呢?
執行緒的代價上面是一個極端的反面教材,我的目的,就是要讓大家意識到,多執行緒不是萬能的。有的時候,開啟10個執行緒,遠遠達不到10倍的執行速度提升的效果。
這背後的原因,是因為建立執行緒是有代價的!而且這個代價非常的昂貴。那不是一般的昂貴!
執行緒需要的記憶體我們再來寫一個測試程式,在程式中開啟一定數量的空執行緒(儘量保證執行緒執行的程式碼不再佔用額外的記憶體),然後調整執行緒的數量,來觀察執行緒的記憶體佔用情況:
static void Main(){ Console.WriteLine("Hello Thread World!"); var thds = new List<System.Threading.Thread>(); var thdnum = 100; // 執行緒數量 for (int i = 0; i < thdnum; i++) { var t = new System.Threading.Thread(TestThread); t.Start(); thds.Add(t); } Console.WriteLine("Thread Finished!"); Console.ReadKey();}static void TestThread(){ Console.ReadKey();}
上面是100個空執行緒,編譯程式,並在Release模式下執行,檢視記憶體的佔用情況如下:
100個執行緒的記憶體佔用
可以看到,佔用的記憶體並不多,只有14.0MB。然後我們將執行緒數量調整為1萬個試試:
1萬個執行緒的記憶體佔用
我們看到記憶體從14.0MB上升到了306.1MB,看起來感覺還不錯,記憶體佔用不是特別離譜。但是我的電腦,已經有明顯的卡頓了!
我掏出計算器算了算,一個執行緒佔用的記憶體約為:0.03MB,也就是30KB的樣子:
單個執行緒平均記憶體佔用
可能有同學要說了,感覺還行,可以接受!但是當我嘗試開啟10萬個執行緒的時候,卡了很久,也沒有成功!而且電腦變得幾乎不可操作了,基本卡住不動了!無奈我只好強制結束了測試程式的程序。
MMP
也就是說,用C#來處理類似遊戲這類長連線的場景的話,其同時連線的使用者數,很難突破10萬。當然,真正的遊戲後臺,一般不會選擇C#來做。而是選擇更高效的C++或者其他語言。
聰明小明,又問道:這30KB的記憶體,執行緒拿去做什麼了呢?
其實就是用來儲存執行緒的上下文的。比如堆疊資訊,區域性變數等等。這此資訊,CPU在進行不同執行緒的切換執行的時候,需要用來恢復執行緒的狀態。
執行緒需要的CPU現在我們再回到最開始的示例,示例中,多執行緒花費的時間,比單執行緒要多得多得多,多了好幾個數量級。這在程式界,是非常大的效能差異。
其實執行緒除了需要記憶體還支撐之外,還需要佔用大量的CPU資源。主要是作業系統排程這些執行緒,執行執行緒的上下文切換等工作,也需要佔用大量CPU資源。
還有很重要的一點,在銷燬執行緒的時候,同樣會耗費大量CPU資源。
所以如果我們的程式中的任務,是非常簡單的任務,千萬不能一個任務開啟一個執行緒。因為執行緒本身的開銷,要遠大於我們的任務本身的開銷。不能為了多執行緒這個漂亮盒子,而去做買櫝還珠的愚蠢買賣。
總結好了,說了這麼多,只是向大家說明,執行緒的記憶體和CPU的佔用情況。記憶體和CPU是我們電腦中非常稀缺的寶貴資源,一定要將好鋼用到刀刃上。而不是將其浪費在毫無意義的多執行緒上下文切換上面。
希望大家可以意識到這個昂貴的代價,不要隨意濫用執行緒。
小明默默的點頭
這時我看到小明,在那默默的望著我點頭,懷裡還抱著個大紅包,這是要送給外老師嗎?
踩坑記錄實在是太基礎了,沒有踩到坑!
下期預告下面是給同學們準備的乾貨,陸續發貨中哦:
ThreadPool
多個執行緒之間的資源共享問題
多執行緒死鎖問題
Task
Parallel
await/async
Linq與PLinq (ParallelEnumerable)
。。。 。。。