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

前文中中我們用實驗的方式驗證了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開銷
  • 測試代碼參見test05
# 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又給葬送掉了。


參考文獻

  • Golang之協程詳解
  • 一個“蠅量級” C 語言協程庫
"


分享到:


相關文章: