協程究竟比執行緒能省多少開銷?

語言: CN / TW / HK

Linux程序和執行緒的上下文切換開銷,大約是3-5us之間。這個開銷確實不算大,但是海量網際網路服務端和一般的計算機程式相比,特點是:

  • 高併發:每秒鐘需要處理成千上萬的使用者請求
  • 週期短:每個使用者處理耗時越短越好,經常是ms級別的
  • 高網路IO:經常需要從其它機器上進行網路IO、如Redis、Mysql等等
  • 低計算:一般CPU密集型的計算操作並不多

即使3-5us的開銷,如果上下文切換量特別大的話,也仍然會顯得是有那麼一些效能低下。例如之前的Web Server之Apache,就是這種模型下的軟體產品。(其實當時Linux作業系統在設計的時候,目標是一個通用的作業系統,並不是專門針對服務端高併發來設計的)

為了避免頻繁的上下文切換,還有一種非同步非阻塞的開發模型。那就是用一個程序或執行緒去接收一大堆使用者的請求,然後通過IO多路複用的方式來提高效能(程序或執行緒不阻塞,省去了上下文切換的開銷)。Nginx和Node Js就是這種模型的典型代表產品。平心而論,從程式執行效率上來,這種模型最為機器友好,執行效率是最高的(比下面提到的協程開發模型要好)。所以Nginx已經取代了Apache成為了Web Server裡的首選。但是這種程式設計模型的問題在於開發不友好,說白了就是過於機器化,離程序概念被抽象出來的初衷背道而馳。人類正常的線性思維被打亂,應用層開發們被逼得以非人類的思維去編寫程式碼,程式碼除錯也變得異常困難。

於是就有一些聰明的腦袋們繼續在應用層又動起了主意,設計出了不需要程序/執行緒上下文切換的“執行緒”,協程。用協程去處理高併發的應用場景,既能夠符合程序涉及的初衷,讓開發者們用人類正常的線性的思維去處理自己的業務,也同樣能夠省去昂貴的程序/執行緒上下文切換的開銷。因此可以說,協程就是Linux處理海量請求應用場景裡的程序模型的一個很好的的補丁。

背景介紹完了,那麼我想說的是,畢竟協程的封裝雖然輕量,但是畢竟還是需要引入了一些額外的代價的。那麼我們來看看這些額外的代價具體多小吧。

協程開銷測試

1、協程切換CPU開銷 測試過程是不斷在協程之間讓出CPU。核心程式碼如下。

func cal()  {
    for i :=0 ; i<1000000 ;i++{
        runtime.Gosched()
    }
}

func main() {
    runtime.GOMAXPROCS(1)  // 單核測試

    currentTime:=time.Now()
    fmt.Println(currentTime)

    go cal()  
    for i :=0 ; i<1000000 ;i++{
        runtime.Gosched()
    }

    currentTime=time.Now()
    fmt.Println(currentTime)
}

編譯執行

# cd tests/test05/src/main/;  
# go build  
# ./main  
2019-08-08 22:35:13.415197171 +0800 CST m=+0.000286059
2019-08-08 22:35:13.655035993 +0800 CST m=+0.240124923

平均每次協程切換的開銷是(655035993-415197171)/2000000=120ns。相對於前面文章測得的程序切換開銷大約3.5us,大約是其的三十分之一。比系統呼叫的造成的開銷還要低。

2、協程記憶體開銷 在空間上,協程初始化建立的時候為其分配的棧有2KB。而執行緒棧要比這個數字大的多,可以通過ulimit 命令檢視,一般都在幾兆,作者的機器上是10M。如果對每個使用者建立一個協程去處理,100萬併發使用者請求只需要2G記憶體就夠了,而如果用執行緒模型則需要10T。

ulimit -a

stack size (kbytes, -s) 10240 //執行緒大小,系統引數配置

本節結論 協程由於是在使用者態來完成上下文切換的,所以切換耗時只有區區100ns多一些,比程序切換要高30倍。單個協程需要的棧記憶體也足夠小,只需要2KB。所以,近幾年來協程大火,在網際網路後端的高併發場景裡大放光彩。

無論是空間還是時間效能都比程序(執行緒)好這麼多,那麼Linus為啥不把它在作業系統裡實現了多好?作業系統為了實現實時性更好的目的,對一些優先順序比較高的程序是會搶佔其它程序的CPU的。而協程無法實現這一點,還得依賴於擋前使用CPU的協程主動釋放,於作業系統的實現目的不相吻合。所以協程的高效是以犧牲可搶佔性為代價的。

由於go的協程呼叫起來太方便了,所以一些go的程式設計師就很隨意地go來go去。要知道go這條指令在切換到協程之前,得先把協程創建出來。而一次建立加上排程開銷就漲到400ns,差不多相當於一次系統呼叫的耗時了。雖然協程很高效,但是也不要亂用,否則go祖師爺Rob Pike花大精力優化出來的效能,被你隨意一go又給葬送掉了。

分享到: