go語言中的main函數也是運行在一個單獨的goroutine中的,通常稱爲 main goroutine,main函數結束時,會打斷其它 goroutine 的執行,可是其它 goroutine 不會打斷其它的 goroutine 的執行,除非是經過通訊讓對方自行停止。緩存
先來看一個最簡單的併發例子,一個時間服務器:(偏題了,不該該使用這個例子,應該突出重點,這個例子能夠放到tcp那節)服務器
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 } go handleConn(conn) } } func handleConn(c net.Conn) { defer c.Close() for { _, err := io.WriteString(c, time.Now().Format("2006/01/02 15:04:05\n")) if err != nil { return } time.Sleep(1 * time.Second) } }
從這個例子能夠看出,使用go開啓一個tcp服務很是簡潔,此外從該例子中,咱們能夠順便了解一下go中格式化日期和時間的方式是很是奇葩的,格式化模板限定爲Mon Jan 2 03:04:05PM 2006 UTC-0700,能夠這樣記:1月2日下午3點4分5秒(200)6年UTC-0700。數據結構
能夠用 nc 或 telnet 來鏈接這個tcp服務進行測試, 也可使用go實現一個簡單的客戶端:併發
func main() { conn, err := net.Dial("tcp", "localhost:8000") if err != nil { log.Fatal(err) } defer conn.Close() mustCopy(os.Stdout, conn) } func mustCopy(dst io.Writer, src io.Reader) { if _, err := io.Copy(dst, src); err != nil { log.Fatal(err) } }
goroutine是經過 channel 來通訊的, 包括髮送和接收兩種操做。能夠經過 make 來聲明一個channel 如 ch = make(chan int),和 map 相似,channel 也只是對應底層數據結構的引用,因此發送方和接收方都將引用同一份對象,channel是引用類型,因此它的零值是 nil。channel 使用的操做符是 <- ,發送操做是指將數據發送到 channel,接收是指從 channel中接收。channel相似於通常的io系統,能夠經過 close(ch) 來關閉一個 channel,關閉後,將不能進行發送操做,但能夠接收以前未接收完的數據,若是沒有數據可接收,則接收到一個nil。tcp
經過 make(chan int) 這種方式建立的 channel 稱之爲無緩存channel,表示 channel的容量是0,若是要聲明一個有緩存的channel,可使用 make(chan int, 100) ,第二個參數表示初始化時的channel容量大小。函數
一個基於無緩存Channels的發送操做將致使發送者goroutine阻塞,直到另外一個goroutine在相同的Channels上執行接收操做,當發送的值經過Channels成功傳輸以後,兩個goroutine能夠繼續執行後面的語句。反之,若是接收操做先發生,那麼接收者goroutine也將阻塞,直到有另外一個goroutine在相同的Channels上執行發送操做。測試
簡單的說,就是使用無緩存channel時,一旦通訊發起,必需要等到通訊完成才能夠繼續執行,不然將阻塞等待。這也就意味着,無緩存channel在通訊時,會強制進行一次同步操做,因此無緩存channel也稱之爲同步channelspa
下面是一個串聯channel的例子:code
func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { for x := 1; x <= 10; x++ { ch1 <- x } close(ch1) }() go func() { for x := range ch1 { ch2 <- x * x } close(ch2) }() for x := range ch2 { fmt.Println(x) } }
無論一個channel是否被關閉,當它沒有被引用時將會被Go語言的垃圾自動回收器回收。(不要將關閉一個打開文件的操做和關閉一個channel操做混淆。對於每一個打開的文件,都須要在不使用的使用調用對應的Close方法來關閉文件。)試圖重複關閉一個channel或者關閉一個nil值的channel都將致使panic異常,此外關閉一個channels還會觸發一個廣播機制。orm
由於關閉操做只用於斷言再也不向channel發送新的數據,因此只有在發送者所在的goroutine纔會調用close函數,所以對一個只接收的channel調用close將是一個編譯錯誤。
單方向channel,只發送或只接收,功能明確。類型chan<- int
表示一個只發送int的channel,只能發送不能接收。相反,類型<-chan int
表示一個只接收int的channel,只能接收不能發送。(箭頭<-
和關鍵字chan的相對位置代表了channel的方向。)這種限制將在編譯期檢測。
func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out) } func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out) } func printer(in <-chan int) { for v := range in { fmt.Println(v) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares) }
帶緩存的 channel,內部持有一個元素隊列。隊列的最大容量是在調用make函數建立channel時經過第二個參數指定的。向緩存Channel的發送操做就是向內部緩存隊列的尾部插入緣由,接收操做則是從隊列的頭部刪除元素。若是內部緩存隊列是滿的,那麼發送操做將阻塞直到因另外一個goroutine執行接收操做而釋放了新的隊列空間。相反,若是channel是空的,接收操做將阻塞直到有另外一個goroutine執行發送操做而向隊列插入元素。
內置的 cap 函數能夠查看某個 channel 的容量,len 函數能夠查看某個 channel 的當前緩存隊列中有效元素的個數。
若是咱們使用了無緩存的channel,那麼兩個慢的goroutines將會由於沒有人接收而被永遠卡住。這種狀況,稱爲goroutines泄漏,這將是一個BUG。和垃圾變量不一樣,泄漏的goroutines並不會被自動回收,所以確保每一個再也不須要的goroutine能正常退出是重要的。
關於無緩存或帶緩存channels之間的選擇,或者是帶緩存channels的容量大小的選擇,均可能影響程序的正確性。無緩存channel更強地保證了每一個發送操做與相應的同步接收操做;可是對於帶緩存channel,這些操做是解耦的。一樣,即便咱們知道將要發送到一個channel的信息的數量上限,建立一個對應容量大小帶緩存channel也是不現實的,由於這要求在執行任何接收操做以前緩存全部已經發送的值。若是未能分配足夠的緩衝將致使程序死鎖。
若是生產線的前期階段一直快於後續階段,那麼它們之間的緩存在大部分時間都將是滿的。相反,若是後續階段比前期階段更快,那麼它們之間的緩存在大部分時間都將是空的。對於這類場景,額外的緩存並無帶來任何好處。
個人理解是,當通訊雙方的工做效率接近時,緩存纔有意義,若是一方處理過快,而另外一方過慢,則效率由短板決定,緩存並不能起到提高效率的做用。