若是把語言比喻爲武俠小說中的武功,若是隻是會用,也就是達到四五層,若是用的熟練也就六七層,若是能見招拆招也得八九層,若是你出神入化,立於不敗之地十層。程序員
若是你想真正掌握一門語言的,怎麼也得八層以上,須要你深刻了解這門語言方方面面的細節。golang
但願之後對Go語言的掌握能有八九層,怎麼能不懂調度器!?編程
Google、百度、微信搜索了許多Go語言調度的文章,這些文章上來就講調度器是什麼樣的,它由哪些組成,它的運做原理,搞的我只能從這些零散的文章中造成調度器的「概貌」,這是我想要的結果,但這還不夠。微信
學習不只要知其然,還要知其因此然。多線程
學習以前,先學知識點的歷史,再學知識,這樣你就明白,爲何它是當下這個樣子。併發
因此,我打算寫一個goroutine調度器的系列文章,從歷史背景講起,按部就班,但願你們能對goroutine調度器有一個全面的認識。app
這篇文章介紹調度器相關的歷史背景,請慢慢翻閱。less
上面這個你們夥是ENIAC,它誕生在賓夕法尼亞大學,是世界第一臺真正的通用計算機,和現代的計算機相比,它是至關的「笨重」,它的計算能力,跟現代人手普及的智能手機相比,簡直是一個天上一個地下,ENIAC在地下,智能手機在天上。函數
它上面沒有操做系統,更別提進程、線程和協程了。高併發
後來,現代化的計算機有了操做系統,每一個程序都是一個進程,可是操做系統在一段時間只能運行一個進程,直到這個進程運行完,才能運行下一個進程,這個時期能夠成爲單進程時代——串行時代。
和ENIAC相比,單進程是有了幾萬倍的提度,但依然是太慢了,好比進程要讀數據阻塞了,CPU就在哪浪費着,偉大的程序員們就想了,不能浪費啊,怎麼才能充分的利用CPU呢?
後來操做系統就具備了最先的併發能力:多進程併發,當一個進程阻塞的時候,切換到另外等待執行的進程,這樣就能儘可能把CPU利用起來,CPU就不浪費了。
多進程真實個好東西,有了對進程的調度能力以後,偉大的程序員又發現,進程擁有太多資源,在建立、切換和銷燬的時候,都會佔用很長的時間,CPU雖然利用起來了,但CPU有很大的一部分都被用來進行進程調度了,怎麼才能提升CPU的利用率呢?
你們但願能有一種輕量級的進程,調度不怎麼花時間,這樣CPU就有更多的時間用在執行任務上。
後來,操做系統支持了線程,線程在進程裏面,線程運行所須要資源比進程少多了,跟進程比起來,切換簡直是「不算事」。
一個進程能夠有多個線程,CPU在執行調度的時候切換的是線程,若是下一個線程也是當前進程的,就只有線程切換,「很快」就能完成,若是下一個線程不是當前的進程,就須要切換進程,這就得費點時間了。
這個時代,CPU的調度切換的是進程和線程。多線程看起來很美好,但實際多線程編程卻像一坨屎,一是因爲線程的設計自己有點複雜,而是因爲須要考慮不少底層細節,好比鎖和衝突檢測。
多進程、多線程已經提升了系統的併發能力,可是在當今互聯網高併發場景下,爲每一個任務都建立一個線程是不現實的,由於會消耗大量的內存(每一個線程的內存佔用級別爲MB),線程多了以後調度也會消耗大量的CPU。偉大的程序員們有開始想了,如何才能充分利用CPU、內存等資源的狀況下,實現更高的併發?
既然線程的資源佔用、調度在高併發的狀況下,依然是比較大的,是否有一種東西,更加輕量?
你可能知道:線程分爲內核態線程和用戶態線程,用戶態線程須要綁定內核態線程,CPU並不能感知用戶態線程的存在,它只知道它在運行1個線程,這個線程實際是內核態線程。
用戶態線程實際有個名字叫協程(co-routine),爲了容易區分,咱們使用協程指用戶態線程,使用線程指內核態線程。
User-level threads, Application-level threads, Green threads都指同樣的東西,就是不受OS感知的線程,若是你Google coroutine相關的資料,會看到它指的就是用戶態線程,在 Green threads的維基百科裏,看Green threads的實現列表,你會看到好不少coroutine實現,好比Java、Lua、Go、Erlang、Common Lisp、Haskell、Rust、PHP、Stackless Python,因此,我認爲用戶態線程就是協程。
協程跟線程是有區別的,線程由CPU調度是搶佔式的,協程由用戶態調度是協做式的,一個協程讓出CPU後,才執行下一個協程。
協程和線程有3種映射關係:
協程是個好東西,很多語言支持了協程,好比:Lua、Erlang、Java(C++即將支持),就算語言不支持,也有庫支持協程,好比C語言的coroutine(風雲大牛做品)、Kotlin的kotlinx.coroutines、Python的gevent。
Go語言的誕生就是爲了支持高併發,有2個支持高併發的模型:CSP和Actor。鑑於Occam和Erlang都選用了CSP(來自Go FAQ),而且效果不錯,Go也選了CSP,但與前二者不一樣的是,Go把channel做爲頭等公民。
就像前面說的多線程編程太不友好了,Go爲了提供更容易使用的併發方法,使用了goroutine和channel。goroutine來自協程的概念,讓一組可複用的函數運行在一組線程之上,即便有協程阻塞,該線程的其餘協程也能夠被runtime
調度,轉移到其餘可運行的線程上。最關鍵的是,程序員看不到這些底層的細節,這就下降了編程的難度,提供了更容易的併發。
Go中,協程被稱爲goroutine(Rob Pike說goroutine不是協程,由於他們並不徹底相同),它很是輕量,一個goroutine只佔幾KB,而且這幾KB就足夠goroutine運行完,這就能在有限的內存空間內支持大量goroutine,支持了更多的併發。雖然一個goroutine的棧只佔幾KB,但實際是可伸縮的,若是須要更多內容,runtime
會自動爲goroutine分配。
終於來到了Go語言的調度器環節。
調度器的任務是在用戶態完成goroutine的調度,而調度器的實現好壞,對併發實際有很大的影響,而且Go的調度器就是M:N類型的,實現起來也是最複雜。
如今的Go語言調度器是2012年從新設計的(設計方案),在這以前的調度器稱爲老調度器,老調度器的實現不太好,存在性能問題,因此用了4年左右就被替換掉了,老調度器大概是下面這個樣子:
最下面是操做系統,中間是runtime,runtime在Go中很重要,許多程序運行時的工做都由runtime完成,調度器就是runtime的一部分,虛線圈出來的爲調度器,它有兩個重要組成:
M想要執行、放回G都必須訪問全局G隊列,而且M有多個,即多線程訪問同一資源須要加鎖進行保證互斥/同步,因此全局G隊列是有互斥鎖進行保護的。
老調度器有4個缺點:
面對以上老調度的問題,Go設計了新的調度器,設計文稿:https://golang.org/s/go11sched
新調度器引入了:
如今,調度器中3個重要的縮寫你都接觸到了,全部文章都用這幾個縮寫,請牢記:
這篇文章的目的不是介紹調度器的實現,而是調度器的一些理念,幫助你後面更好理解調度器的實現,因此咱們迴歸到調度器設計思想上。
調度器的有兩大思想:
複用線程:協程自己就是運行在一組線程之上,不須要頻繁的建立、銷燬線程,而是對線程的複用。在調度器中複用線程還有2個體現:1)work stealing,當本線程無可運行的G時,嘗試從其餘線程綁定的P偷取G,而不是銷燬線程。2)hand off,當本線程由於G進行系統調用阻塞時,線程釋放綁定的P,把P轉移給其餘空閒的線程執行。
利用並行:GOMAXPROCS設置P的數量,當GOMAXPROCS大於1時,就最多有GOMAXPROCS個線程處於運行狀態,這些線程可能分佈在多個CPU核上同時運行,使得併發利用並行。另外,GOMAXPROCS也限制了併發的程度,好比GOMAXPROCS = 核數/2,則最多利用了一半的CPU核進行並行。
調度器的兩小策略:
搶佔:在coroutine中要等待一個協程主動讓出CPU才執行下一個協程,在Go中,一個goroutine最多佔用CPU 10ms,防止其餘goroutine被餓死,這就是goroutine不一樣於coroutine的一個地方。
全局G隊列:在新的調度器中依然有全局G隊列,但功能已經被弱化了,當M執行work stealing從其餘P偷不到G時,它能夠從全局G隊列獲取G。
上面提到並行了,關於併發和並行再說一下:Go創始人Rob Pike一直在強調go是併發,不是並行,由於Go作的是在一段時間內完成幾十萬、甚至幾百萬的工做,而不是同一時間同時在作大量的工做。併發能夠利用並行提升效率,調度器是有並行設計的。
並行依賴多核技術,每一個核上在某個時間只能執行一個線程,當咱們的CPU有8個核時,咱們能同時執行8個線程,這就是並行。
這篇文章的主要目的是爲後面介紹Go語言調度器作鋪墊,由遠及近的方式簡要介紹了多進程、多線程、協程、併發和並行有關的「史料」,但願你瞭解爲何Go採用了goroutine,又爲什麼調度器如此重要。
若是你等不急了,想了解Go調度器相關的原理,看下這些文章:
聲明:關於老調度器的資料已經徹底搜不到,根據新版調度器設計方案的描述,想象着寫了老調度器這一章,可能存在錯誤。
- 若是這篇文章對你有幫助,請點個贊,感謝。
- 本文做者:大彬
- 若是喜歡本文,隨意轉載,但請保留此原文連接:http://lessisbetter.site/2019/03/10/golang-scheduler-1-history