協程究竟比線程能省多少開銷?

前文中中咱們用實驗的方式驗證了Linux進程和線程的上下文切換開銷,大約是3-5us之間。當運行在通常的計算機程序時,這個開銷確實不算大。可是海量互聯網服務端和通常的計算機程序相比,特色是:php

  • 高併發:每秒鐘須要處理成千上萬的用戶請求
  • 週期短:每一個用戶處理耗時越短越好,常常是ms級別的
  • 高網絡IO:常常須要從其它機器上進行網絡IO、如Redis、Mysql等等
  • 低計算:通常CPU密集型的計算操做並很少

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

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

因而就有一些聰明的腦殼們繼續在應用層又動起了主意,設計出了不須要進程/線程上下文切換的「線程」,協程。用協程去處理高併發的應用場景,既可以符合進程涉及的初衷,讓開發者們用人類正常的線性的思惟去處理本身的業務,也一樣可以省去昂貴的進程/線程上下文切換的開銷。所以能夠說,協程就是Linux處理海量請求應用場景裏的進程模型的一個很好的的補丁。sql

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

協程開銷測試

一、協程切換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,大約是其的三十分之一。比系統調用的形成的開銷還要低。網絡

二、協程內存開銷

在空間上,協程初始化建立的時候爲其分配的棧有2KB。而線程棧要比這個數字大的多,能夠經過ulimit 命令查看,通常都在幾兆,做者的機器上是10M。若是對每一個用戶建立一個協程去處理,100萬併發用戶請求只須要2G內存就夠了,而若是用線程模型則須要10T。併發

# ulimit -a  
stack size              (kbytes, -s) 10240

本節結論

協程因爲是在用戶態來完成上下文切換的,因此切換耗時只有區區100ns多一些,比進程切換要高30倍。單個協程須要的棧內存也足夠小,只須要2KB。因此,近幾年來協程大火,在互聯網後端的高併發場景裏大放光彩。異步

不管是空間仍是時間性能都比進程(線程)好這麼多,那麼Linus爲啥不把它在操做系統裏實現了多好?

實際上協程並非一個新玩意,在上個世紀60年代的時候就已經有人提出了。操做系統的一個主要設計目標是實時性,對優先級比較高的進程是會搶佔當前佔用CPU的進程。可是協程沒法實現這一點,還得依賴於使用CPU的一方主動釋放,與操做系統的實現目的不相吻合。協程的高效是以犧牲了可搶佔性爲代價的。

擴展:因爲go的協程調用起來太方便了,因此一些go的程序員就很隨意地go來go去。要知道go這條指令在切換到協程以前,得先把協程建立出來。而一次建立加上調度開銷就漲到400ns,差很少至關於一次系統調用的耗時了。雖然協程很高效,可是也不要亂用,不然go祖師爺Rob Pike花大精力優化出來的性能,被你隨意一go又給葬送掉了。

file


開發內功修煉之CPU篇專輯:


個人公衆號是「開發內功修煉」,在這裏我不是單純介紹技術理論,也不僅介紹實踐經驗。而是把理論與實踐結合起來,用實踐加深對理論的理解、用理論提升你的技術實踐能力。歡迎你來關注個人公衆號,也請分享給你的好友~~~

相關文章
相關標籤/搜索