Golang入門(4):併發

摘要

併發程序指同時進行多個任務的程序,隨着硬件的發展,併發程序變得愈來愈重要。Web服務器會一次處理成千上萬的請求,這也是併發的必要性之一。Golang的併發控制比起Java來講,簡單了很多。在Golang中,沒有多線程這一說法,只有協程,而新建一個協程,僅僅只須要使用go關鍵字。並且,與Java不一樣的是,在Golang中不以共享內存的方式來通訊,而是以經過通訊的方式來共享內存。這方面的內容也比較簡單。html

1 線程與協程

在Golang中,併發是以協程的方式實現的。程序員

在Java中,咱們經常提到線程池,多線程這些概念。然而,在Golang中的協程,和這些是不同的。因此在本文中,先對這幾個概念進行區分。編程

簡單來講,進程和線程是由操做系統進行調度的,協程是對內核透明,由程序本身調度的。不只如此,Golang的協程所佔用的內存空間極小,也就是說,協程更加的輕量。此外,協程的切換通常由程序員在代碼中顯式控制,而不是交給操做系統去調度。它避免了上下文切換時的額外耗費,兼顧了多線程的優勢,簡化了高併發程序的複雜。小程序

至於別的,本文不進行深刻的研究,本文的基調仍是以入門爲主,即怎麼去用服務器

2 goroutine

簡單來講,咱們所編寫的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關鍵字。

3 channel

注意,在Golang的併發中有一項很重要的特性,不要以共享內存的方式來通訊,相反,要經過通訊來共享內存。

這裏說到的通訊方式,指得就是channel,信道。

Channel是Go中的一個核心類型,咱們能夠把理解爲是一種指定了大小和容量的管道。咱們能夠在這個管道的一邊放入數據,在另外一半拿出數據。舉個簡單的例子:

package main

import "fmt"

func main() {
   
   messages := make(chan string)

   go func() { messages <- "ping" }()

   msg := <-messages
   fmt.Println(msg)
}

在這裏須要說明幾點:

  • 信道須要使用make的方式建立,除了可以指定類型,還能在第二個參數指定容量,不然默認爲1,也就是說這是一個同步信道
  • 消息的傳遞和獲取必須成對出現,傳數據用channel <- data,取數據用<- channel
  • 信道是會阻塞的,並且無論傳仍是取,必阻塞,直到另外的goroutine傳或者取爲止。
  • 對於阻塞,能夠理解爲是一個管道中已經有了東西,那麼只有管道爲空了,才能繼續工做

4 range

對於上面提到的信道操做,存在這麼幾個問題:

  • 應該什麼時候中止等待數據?
  • 還會有更多的數據麼,仍是全部內容都已經傳輸完成?
  • 我應該繼續等待仍是該作別的了?

固然,咱們能夠選擇不斷檢查信道,直到他關閉爲止。

可是咱們有更加優雅的解決方案。使用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發現信道被關閉,因而結束。這也就實現了接收器不知道具體須要接收多少個蛋糕的狀況下,可以自動結束的功能。

5 select

select關鍵字用在有多個信道的狀況下。

他的目的是爲了提升系統的效率,而不至於在某一個信道阻塞的狀況下,不知道該幹什麼。

select中會有case代碼塊,用於發送或接收數據。語法以下:

select {
case i := <-c:
    //...
case ...
default:
    //...
}

注意,每個case,必須是一個信道IO指令,default命令塊不是必須。

規律以下:

  • 若是任意一個case代碼塊準備好發送或接收,執行對應內容
  • 若是多餘一個case代碼塊準備好發送或接收,隨機選取一個並執行對應內容
  • 若是任何一個case代碼塊都沒有準備好,等待
  • 若是有default代碼塊,而且沒有任何case代碼塊準備好,執行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:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~

相關文章
相關標籤/搜索