首頁>其它>

買櫝還珠

我們說到,多執行緒,可以利用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)

。。。 。。。

116
  • 康明斯6bt發動機
  • 這大概就是很多知名996網際網路大廠的真實情況