備註:須要區分進程、線程(內核級線程)、協程(用戶級線程)三個概念。linux
進程、線程 和 協程 之間概念的區別程序員
對於 進程、線程,都是有內核進行調度,有 CPU 時間片的概念,進行 搶佔式調度(有多種調度算法)golang
對於 協程(用戶級線程),這是對內核透明的,也就是系統並不知道有協程的存在,是徹底由用戶本身的程序進行調度的,由於是由用戶程序本身控制,那麼就很難像搶佔式調度那樣作到強制的 CPU 控制權切換到其餘進程/線程,一般只能進行 協做式調度,須要協程本身主動把控制權轉讓出去以後,其餘協程才能被執行到。算法
goroutine 和協程區別數據庫
本質上,goroutine 就是協程。 不一樣的是,Golang 在 runtime、系統調用等多方面對 goroutine 調度進行了封裝和處理,當遇到長時間執行或者進行系統調用時,會主動把當前 goroutine 的CPU (P) 轉讓出去,讓其餘 goroutine 能被調度並執行,也就是 Golang 從語言層面支持了協程。Golang 的一大特點就是從語言層面原生支持協程,在函數或者方法前面加 go關鍵字就可建立一個協程。編程
其餘方面的比較windows
1. 內存消耗方面後端
每一個 goroutine (協程) 默認佔用內存遠比 Java 、C 的線程少。
goroutine:2KB
線程:8MB數組
2. 線程和 goroutine 切換調度開銷方面服務器
線程/goroutine 切換開銷方面,goroutine 遠比線程小
線程:涉及模式切換(從用戶態切換到內核態)、16個寄存器、PC、SP...等寄存器的刷新等。
goroutine:只有三個寄存器的值修改 - PC / SP / DX.
線程是操做系統的內核對象,多線程編程時,若是線程數過多,就會致使頻繁的上下文切換,這些 cpu 時間是一個額外的耗費。因此在一些高併發的網絡服務器編程中,使用一個線程服務一個 socket 鏈接是很不明智的。因而操做系統提供了基於事件模式的異步編程模型。用少許的線程來服務大量的網絡鏈接和I/O操做。可是採用異步和基於事件的編程模型,複雜化了程序代碼的編寫,很是容易出錯。由於線程穿插,也提升排查錯誤的難度。
協程,是在應用層模擬的線程,他避免了上下文切換的額外耗費,兼顧了多線程的優勢。簡化了高併發程序的複雜度。舉個例子,一個高併發的網絡服務器,每個socket鏈接進來,服務器用一個協程來對他進行服務。代碼很是清晰。並且兼顧了性能。
那麼,協程是怎麼實現的呢?
他和線程的原理是同樣的,當 a線程 切換到 b線程 的時候,須要將 a線程 的相關執行進度壓入棧,而後將 b線程 的執行進度出棧,進入 b線程 的執行序列。協程只不過是在 應用層 實現這一點。可是,協程並非由操做系統調度的,並且應用程序也沒有能力和權限執行 cpu 調度。怎麼解決這個問題?
答案是,協程是基於線程的。內部實現上,維護了一組數據結構和 n 個線程,真正的執行仍是線程,協程執行的代碼被扔進一個待執行隊列中,由這 n 個線程從隊列中拉出來執行。這就解決了協程的執行問題。那麼協程是怎麼切換的呢?答案是:golang 對各類 io函數 進行了封裝,這些封裝的函數提供給應用程序使用,而其內部調用了操做系統的異步 io函數,當這些異步函數返回 busy 或 bloking 時,golang 利用這個時機將現有的執行序列壓棧,讓線程去拉另一個協程的代碼來執行,基本原理就是這樣,利用並封裝了操做系統的異步函數。包括 linux 的 epoll、select 和 windows 的 iocp、event 等。
因爲golang是從編譯器和語言基礎庫多個層面對協程作了實現,因此,golang的協程是目前各種有協程概念的語言中實現的最完整和成熟的。十萬個協程同時運行也毫無壓力。關鍵咱們不會這麼寫代碼。可是整體而言,程序員能夠在編寫 golang 代碼的時候,能夠更多的關注業務邏輯的實現,更少的在這些關鍵的基礎構件上耗費太多精力。
協程(Coroutine)是在1963年由Melvin E. Conway USAF, Bedford, MA等人提出的一個概念。並且協程的概念是早於線程(Thread)提出的。可是因爲協程是非搶佔式的調度,沒法實現公平的任務調用。也沒法直接利用多核優點。所以,咱們不能武斷地說協程是比線程更高級的技術。
儘管,在任務調度上,協程是弱於線程的。可是在資源消耗上,協程則是極低的。一個線程的內存在 MB 級別,而協程只須要 KB 級別。並且線程的調度須要內核態與用戶的頻繁切入切出,資源消耗也不小。
咱們把協程的基本特色概括爲:
1. 協程調度機制沒法實現公平調度 2. 協程的資源開銷是很是低的,一臺普通的服務器就能夠支持百萬協程。
那麼,近幾年爲什麼協程的概念能夠大熱。我認爲一個特殊的場景使得協程可以普遍的發揮其優點,而且屏蔽掉了劣勢 --> 網絡編程。與通常的計算機程序相比,網絡編程有其獨有的特色。
1. 高併發(每秒鐘上千數萬的單機訪問量) 2. Request/Response。程序生命期端(毫秒,秒級) 3. 高IO,低計算(鏈接數據庫,請求API)。
最開始的網絡程序其實就是一個線程一個請求設計的(Apache)。後來,隨着網絡的普及,誕生了C10K問題。Nginx 經過單線程異步 IO 把網絡程序的執行流程進行了亂序化,經過 IO 事件機制最大化的保證了CPU的利用率。
至此,現代網絡程序的架構已經造成。基於IO事件調度的異步編程。其表明做恐怕就屬 NodeJS
了吧。
異步編程爲了追求程序的性能,強行的將線性的程序打亂,程序變得很是的混亂與複雜。對程序狀態的管理也變得異常困難。寫過Nginx C Module的同窗應該知道我說的是什麼。咱們開始吐槽 NodeJS
那噁心的層層Callback。
在咱們瘋狂被 NodeJS
的層層回調噁心到的時候,Golang
做爲名門以後開始走入咱們的視野。而且迅速的在Web後端極速的跑馬圈地。其表明者 Docker 以及圍繞這 Docker 展開的整個容器生態圈欣欣向榮起來。其最大的賣點 – 協程 開始真正的流行與討論起來。
咱們開始向寫PHP同樣來寫全異步IO的程序。看上去美好極了,彷彿世界就是這樣了。
在網絡編程中,咱們能夠理解爲 Golang
的協程本質上其實就是對 IO 事件的封裝,而且經過語言級的支持讓異步的代碼看上去像同步執行的同樣。
咱們知道,協程(coroutine)是Go語言中的輕量級線程實現,由Go運行時(runtime)管理。
在一個函數調用前加上go關鍵字,此次調用就會在一個新的goroutine中併發執行。當被調用的函數返回時,這個goroutine也自動結束。須要注意的是,若是這個函數有返回值,那麼這個返回值會被丟棄。
先看一下下面的程序代碼:
func Add(x, y int) { z := x + y fmt.Println(z) } func main() { for i:=0; i<10; i++ { go Add(i, i) } }
執行上面的代碼,會發現屏幕什麼也沒打印出來,程序就退出了。
對於上面的例子,main()函數啓動了10個goroutine,而後返回,這時程序就退出了,而被啓動的執行 Add() 的 goroutine 沒來得及執行。咱們想要讓 main() 函數等待全部 goroutine 退出後再返回,但如何知道 goroutine 都退出了呢?這就引出了多個goroutine之間通訊的問題。
在工程上,有兩種最多見的併發通訊模型:共享內存 和 消息。
下面的例子,使用了鎖變量(屬於一種共享內存)來同步協程,事實上 Go 語言主要使用消息機制(channel)來做爲通訊模型
package main import ( "fmt" "sync" "runtime" ) var counter int = 0 func Count(lock *sync.Mutex) { lock.Lock() // 上鎖 counter++ fmt.Println("counter =", counter) lock.Unlock() // 解鎖 } func main() { lock := &sync.Mutex{} for i:=0; i<10; i++ { go Count(lock) } for { lock.Lock() // 上鎖 c := counter lock.Unlock() // 解鎖 runtime.Gosched() // 出讓時間片 if c >= 10 { break } } }
消息機制認爲每一個併發單元是自包含的、獨立的個體,而且都有本身的變量,但在不一樣併發單元間這些變量不共享。每一個併發單元的輸入和輸出只有一種,那就是消息。
channel 是 Go 語言在語言級別提供的 goroutine 間的通訊方式,咱們可使用 channel 在多個 goroutine 之間傳遞消息。channel是進程內的通訊方式,所以經過 channel 傳遞對象的過程和調用函數時的參數傳遞行爲比較一致,好比也能夠傳遞指針等。channel 是類型相關的,一個 channel 只能傳遞一種類型的值,這個類型須要在聲明 channel 時指定。
channel的聲明形式爲:
var chanName chan ElementType
舉個例子,聲明一個傳遞int類型的channel:
var ch chan int
使用內置函數 make() 定義一個channel:
ch := make(chan int)
在channel的用法中,最多見的包括寫入和讀出:
// 將一個數據value寫入至channel,這會致使阻塞,直到有其餘goroutine從這個channel中讀取數據 ch <- value // 從channel中讀取數據,若是channel以前沒有寫入數據,也會致使阻塞,直到channel中被寫入數據爲止 value := <-ch
默認狀況下,channel的接收和發送都是阻塞的,除非另外一端已準備好。
咱們還能夠建立一個帶緩衝的channel:
c := make(chan int, 1024) // 從帶緩衝的channel中讀數據 for i:=range c { ... }
此時,建立一個大小爲1024的int類型的channel,即便沒有讀取方,寫入方也能夠一直往channel裏寫入,在緩衝區被填完以前都不會阻塞。
能夠關閉再也不使用的channel:
close(ch)
應該在生產者的地方關閉channel,若是在消費者的地方關閉,容易引發panic;
如今利用channel來重寫上面的例子:
func Count(ch chan int) { ch <- 1 fmt.Println("Counting") } func main() { chs := make([] chan int, 10) for i:=0; i<10; i++ { chs[i] = make(chan int) go Count(chs[i]) } for _, ch := range(chs) { <-ch } }
在這個例子中,定義了一個包含10個channel的數組,並把數組中的每一個channel分配給10個不一樣的goroutine。在每一個goroutine完成後,向goroutine寫入一個數據,在這個channel被讀取前,這個操做是阻塞的。在全部的goroutine啓動完成後,依次從10個channel中讀取數據,在對應的channel寫入數據前,這個操做也是阻塞的。這樣,就用channel實現了相似鎖的功能,並保證了全部goroutine完成後main()才返回。
另外,咱們在將一個channel變量傳遞到一個函數時,能夠經過將其指定爲單向channel變量,從而限制該函數中能夠對此channel的操做。
在UNIX中,select()函數用來監控一組描述符,該機制常被用於實現高併發的socket服務器程序。Go語言直接在語言級別支持select關鍵字,用於處理異步IO問題,大體結構以下:
select { case <- chan1: // 若是chan1成功讀到數據 case chan2 <- 1: // 若是成功向chan2寫入數據 default: // 默認分支 }
select默認是阻塞的,只有當監聽的channel中有發送或接收能夠進行時纔會運行,當多個channel都準備好的時候,select是隨機的選擇一個執行的。
Go語言沒有對channel提供直接的超時處理機制,但咱們能夠利用select來間接實現,例如:
timeout := make(chan bool, 1) go func() { time.Sleep(1e9) timeout <- true }() switch { case <- ch: // 從ch中讀取到數據 case <- timeout: // 沒有從ch中讀取到數據,但從timeout中讀取到了數據 }
這樣使用select就能夠避免永久等待的問題,由於程序會在timeout中獲取到一個數據後繼續執行,而不管對ch的讀取是否還處於等待狀態。