併發程序指同時進行多個任務的程序,隨着硬件的發展,併發程序變得愈來愈重要。Web服務器會一次處理成千上萬的請求,這也是併發的必要性之一。Golang的併發控制比起Java來講,簡單了很多。在Golang中,沒有多線程這一說法,只有協程,而新建一個協程,僅僅只須要使用go
關鍵字。並且,與Java不一樣的是,在Golang中不以共享內存的方式來通訊,而是以經過通訊的方式來共享內存。這方面的內容也比較簡單。html
在Golang中,併發是以協程的方式實現的。程序員
在Java中,咱們經常提到線程池,多線程這些概念。然而,在Golang中的協程,和這些是不同的。因此在本文中,先對這幾個概念進行區分。編程
簡單來講,進程和線程是由操做系統進行調度的,協程是對內核透明,由程序本身調度的。不只如此,Golang的協程所佔用的內存空間極小,也就是說,協程更加的輕量。此外,協程的切換通常由程序員在代碼中顯式控制,而不是交給操做系統去調度。它避免了上下文切換時的額外耗費,兼顧了多線程的優勢,簡化了高併發程序的複雜。小程序
至於別的,本文不進行深刻的研究,本文的基調仍是以入門爲主,即怎麼去用。服務器
簡單來講,咱們所編寫的Golang源代碼所有都跑在goroutine中。網絡
咱們只須要使用go
關鍵字,就能夠啓動一個goroutine。多線程
package main import "fmt" func f(msg string) { fmt.Println(msg) } func main(){ go f("hello goroutine") }
至於其他的事情,就交給Golang的runtime了,Go的runtime負責對goroutine進行調度。簡單的來說,調度就是決定哪一個goroutine將得到資源開始執行、哪一個goroutine應該中止執行讓出資源、哪一個goroutine應該被喚醒恢復執行等。併發
咱們下面寫個小例子,來看看Golang如何編寫併發的小程序:curl
package main import ( "io" "log" "net" "time" ) func main() { listener, err := net.Listen("tcp", "localhost:8000") if err != nil { log.Fatal(err) } for { conn, err := listener.Accept() if err != nil { log.Print(err) // 假設出現了錯誤 continue } handleConn(conn) // 處理鏈接 } } func handleConn(c net.Conn) { defer c.Close() for { _, err := io.WriteString(c, time.Now().Format("15:04:05\n")) if err != nil { return // 鏈接關閉,則中止執行 } time.Sleep(1 * time.Second) } }
簡單解釋一下,這個來自於這裏的小例子中,咱們監聽了本地8000端口的TCP鏈接。而後,當有鏈接過來的時候,每隔一秒將當前的時間打印在屏幕上。tcp
在Windows中,咱們可使用curl
命令來測試:
curl 127.0.0.1:8000
效果以下:
可是問題來了,若是咱們再打開一個CMD窗口,去創建一個TCP鏈接,是失敗的。除非將原來的那個鏈接中斷,Golang才能接受新的鏈接。否則,新的鏈接將一直被阻塞。
能夠看到,若是同時啓動兩個鏈接,只有一個鏈接能夠提供打印時間的服務,另外一個鏈接將被阻塞:
這時,將第一個鏈接中斷,則第二個鏈接才能夠進行打印:
在這個時候,咱們只須要在調用handleConn(conn)
這個函數以前,加上go
的關鍵字,就能夠實現併發了。
部分代碼以下:
for { conn, err := listener.Accept() if err != nil { log.Print(err) // 假設出現了錯誤 continue } go handleConn(conn) // 處理鏈接 }
隨後,咱們就能夠處理多個鏈接了:
因此,在Golang中實現併發,就是這麼的簡單。咱們須要作的,就是在調用須要建立協程的函數前面,加上go
關鍵字。
注意,在Golang的併發中有一項很重要的特性,不要以共享內存的方式來通訊,相反,要經過通訊來共享內存。
這裏說到的通訊方式,指得就是channel,信道。
Channel是Go中的一個核心類型,咱們能夠把理解爲是一種指定了大小和容量的管道。咱們能夠在這個管道的一邊放入數據,在另外一半拿出數據。舉個簡單的例子:
package main import "fmt" func main() { messages := make(chan string) go func() { messages <- "ping" }() msg := <-messages fmt.Println(msg) }
在這裏須要說明幾點:
channel <- data
,取數據用<- channel
。對於上面提到的信道操做,存在這麼幾個問題:
固然,咱們能夠選擇不斷檢查信道,直到他關閉爲止。
可是咱們有更加優雅的解決方案。使用range
關鍵字,使用在channel上時,會自動等待channel的動做一直到channel被關閉。下面來看一個小例子,這個例子來源於簡書:
package main import ( "fmt" "time" "strconv" ) func makeCakeAndSend(cs chan string, count int) { for i := 1; i <= count; i++ { cakeName := "Strawberry Cake " + strconv.Itoa(i) cs <- cakeName //將蛋糕送入cs } close(cs) } func receiveCakeAndPack(cs chan string) { for s := range cs { fmt.Println("Packing received cake: ", s) } } func main() { cs := make(chan string) go makeCakeAndSend(cs, 5) go receiveCakeAndPack(cs) //讓程序不會立刻結束,以達到查看輸出結果的目的 time.Sleep(3 * 1e9) }
在這裏,咱們定義了一個同步信道。
在製做蛋糕的過程當中,咱們使用了一個for循環,不斷的將蛋糕送入cs
中。
注意,這裏由於是同步信道,因此並非將五個蛋糕所有制做完,再所有一塊兒接收的,而是製做一個,接受一個。
最後,咱們關閉這個信道,隨後range
發現信道被關閉,因而結束。這也就實現了接收器不知道具體須要接收多少個蛋糕的狀況下,可以自動結束的功能。
select
關鍵字用在有多個信道的狀況下。
他的目的是爲了提升系統的效率,而不至於在某一個信道阻塞的狀況下,不知道該幹什麼。
select中會有case代碼塊,用於發送或接收數據。語法以下:
select { case i := <-c: //... case ... default: //... }
注意,每個case,必須是一個信道IO指令,default命令塊不是必須。
規律以下:
咱們仍是以上面作蛋糕爲例,可是此次能夠同時作草莓味和巧克力味的蛋糕了:
package main import ( "fmt" "strconv" "time" ) func makeCakeAndSend(cs chan string, flavor string, count int) { for i := 1; i <= count; i++ { cakeName := flavor + "蛋糕 " + strconv.Itoa(i) cs <- cakeName //send a strawberry cake } close(cs) } func receiveCakeAndPack(strbry_cs chan string, choco_cs chan string) { strbry_closed, choco_closed := false, false for { //若是兩個信道都關閉了,說明製做完成,結束程序 if (strbry_closed && choco_closed) { return } fmt.Println("等待新蛋糕 ...") select { case cakeName, strbry_ok := <-strbry_cs: if (!strbry_ok) { strbry_closed = true fmt.Println(" ... 草莓信道關閉") } else { fmt.Println("在草莓信道中收到一個新蛋糕。名爲:", cakeName) } case cakeName, choco_ok := <-choco_cs: if (!choco_ok) { choco_closed = true fmt.Println(" ... 巧克力信道關閉") } else { fmt.Println("在巧克力信道中收到一個新蛋糕。名爲:", cakeName) } } } } func main() { strbry_cs := make(chan string) choco_cs := make(chan string) //two cake makers go makeCakeAndSend(choco_cs, "巧克力", 3) //製做3個巧克力蛋糕,而後發送 go makeCakeAndSend(strbry_cs, "草莓", 3) //製做3個草莓蛋糕,而後發送 //one cake receiver and packer go receiveCakeAndPack(strbry_cs, choco_cs) //收穫 //查看結果 time.Sleep(2 * 1e9) }
在這裏,由於咱們是不知道哪一種口味的蛋糕已經被製做完成的,因此咱們使用了select
。只要這個case被激活了,那麼就會完成後面的代碼。也就是說,當某種口味的蛋糕被製做完成以後,就會被收取。
注意,咱們這裏使用的多個返回值
case cakeName, strbry_ok := <- strbry_cs
第二個返回值是一個bool類型,當其爲false時說明channel被關閉了。若是是true,說明有一個值被成功傳遞了。
咱們使用能夠這個值來判斷是否應該中止等待。
至此,《Golang入門》系列已經結束。
謝謝你可以看到這裏。
做者大概花了一週的時間,學習Golang,而且將本身學習的內容以博客的形式分享出來,但願可以給你們一些幫助。
固然了,在這期間必定會有不少疏漏,但願你們能夠指正。其次,也不少地方沒有深究,這是由於做者這個系列的文章只是想先對Golang有一個總體的認識,至於其餘的,在用到的時候,再深刻進行挖掘。
日後的內容,做者可能會考慮Golang網絡編程方面,也可能考慮Golang源碼方面,或者說Golang的各類包系列,這個等做者研究研究,再與你們進行分享。再遠一點,像Redis相關,MySQL相關,系統底層如操做系統,計網等,也都會進行介紹。
扯遠了,flag立了不少(笑)
那麼接下來,也請各位多多指教。
謝謝啦~
PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~