初始Golang通道

文檔翻譯自Golang channels tutorialhtml

Golag內置併發功能的特性。經過把標識符go放置在將要執行的函數前,被執行的代碼能夠在相同的地址空間內啓動獨立的併發線程。在Golang中,稱爲goroutine。這裏我想特別強調下併發並不意味着並行。Goroutines是建立併發體系結構的手段,能夠在硬件容許的狀況下並行執行。併發並不意味着並行golang

咱們先試試goroutine的示例代碼:數組

func main() {
     // Start a goroutine and execute println concurrently
     go println("goroutine message")
     println("main function message")
}
複製代碼

這段程序將會打印main function message,可是可能打印goroutine function message。在main方法中衍生goroutine協程,可是在main方法中並無等待goroutine方法執行完,此時程序就只會打印main function message,反之會打印出goroutine message。緩存

你想必會想golang必定會有解決這種不肯定性的方案,這就是我將要介紹的關於Golang通道的知識點。bash

通道的基礎知識

通道能夠將併發執行的程序同步,並經過特定值類型實現併發進程相互通訊。通道有如下幾個組成:經過通道傳遞的數據類型、通道的緩存容量、標識數據方向的標識符<-。開發者能夠經過內置函數make爲通道分配內存。併發

i := make(chan int)
s := make(chan string, 3)

r := make(<-chan bool) // can only read from
w := make(chan<- []os.FileInfo) // can only write to
複製代碼

通道是基礎的變量,使用場景與其它基礎類型同樣。諸如:結構元素、函數變量、函數返回值、甚至能夠做爲其它通道的類型:異步

c := make(chan <- chan bool)

func readFromChannel(input <-chan string)

func getChannel() chan bool {
    b := make(chan bool)
    return b
}
複製代碼

能夠經過標識符<-向通道中讀寫數據,如何建立通道並對它作些基礎操做,如今讓咱們回到本文的第一個例子程序中,探究通道能夠幫助咱們事先什麼功能?函數

package main
import (
	"fmt"
)

func main() {
	done := make(chan bool)

	go func() {
		fmt.Println("Goroutine message")
		done <- true
	}()

	fmt.Println("Main message")
	<-done
}
複製代碼

這段程序將會將程序中全部message信息都打印出來,並不存在其它可能性。爲何會這樣?done通道沒有任何緩衝(因爲咱們沒有爲通道定義容量)。全部沒有緩衝的通道都將會阻塞程序的執行,除非通道的發送和接受數據的功能都已經爲通訊作好準備,這也是無緩衝通道也被稱爲同步的緣由。在咱們的示例代碼main函數中,消費done通道的數據一直會阻塞main函數的執行,直至goroutine向main通道中寫入數據。所以程序只有在成功讀取done通道的數據後纔會結束。ui

對於有緩衝的通道:當緩存不爲空時,全部經過通道讀數據的操做都不會阻塞程序的執行。當緩存沒有滿時,全部向通道中寫數據的操做都不會阻塞程序的執行。這樣的通道能夠稱爲異步。下面的代碼將展現同步通道與異步通道的差別:this

package main

import (
	"fmt"
	"time"
)

func main() {
	message := make(chan string)
	count := 3

	go func () {
		for i := 1; i <= count; i ++ {
			fmt.Println("send message")
			message <- fmt.Sprintf("message %d", i)
		}
	}()

	time.Sleep(time.Second * 3)
	for i := 1; i <= count; i ++ {
		fmt.Println(<-message)
	}
}
複製代碼

上面的例子中message通道是同步的,程序的輸出以下:

send message
// wait for 3 seconds
message 1
send message
send message
message 2
message 3
複製代碼

如你所見,當第一次向通道中寫入數據後,其它的寫入數據操做都將被阻塞,直到3秒後,通道中的數據被消費。

若是咱們使用的是有緩存的通道, 例如向下面那樣建立message通道: message := make(chan tring, 2) 這時,程序將是下面的輸出:

send message
send message
send message

// wait for 3 seconds
message 1
message 2
message 3
複製代碼

經過上面的輸出咱們能夠看到,全部緩存通道的寫操做並無由於通道中的數據沒有被消費而阻塞。經過更改通道的容量,開發者能夠控制系統吞吐量。

死鎖

如今咱們先看下面這段向通道中讀寫數據的示例代碼片斷:

package main
import (
    "fmt"
)

func main() {
    c :=  make(chan int)
    c <- 42
    val := <-c
    fmt.Println(val)
}
複製代碼

執行上面的代碼,將會獲得下面的輸出結果:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
     /fullpathtofile/channelsio.go:5 +0x54
exit status 2
複製代碼

上面的輸出結果是典型的死鎖信息。這是因爲兩個goroutines相互等待對方,以致於任何一方都不能完成執行操做。Golang能夠在運行時發現錯誤,而後將錯誤信息打印出來。所以這個錯誤是因爲通道的通訊被阻塞所致使的。

代碼能夠在單線程中按照順序一行行的執行,只有當接收方順利讀取通道中的數據後,(c <- 42)才能夠以同步的方式向通道中寫入數據。所以開發者要在向通道寫數據的代碼後,添加向通道中消費數據的功能代碼。

爲了使程序順利運行,開發者能夠參考下面的修改:

package main
import (
    "fmt"
)

func main() {
    c := make(chan int)
    
    go func() {
        c <- 42
    }()
    
    val := <-c
    fmt.Println(val)
}
複製代碼

Range channels and closing(很是抱歉,真心不知道給如何優雅的翻譯)

在上面的了例子中,實現屢次經過通道讀寫數據的代碼以下:

for i := 1; i <= count; i ++ {
    fmt.Println(<-message)
}
複製代碼

因爲通道被消費的數據個數不能多於寫入通道的數據個數,爲了順利經過通道讀數據並且不產生死鎖,開發者必須準確知道已經向通道中寫入多少條數據。

在Golang中,經過range表達式的語法能夠實現對數組、字符串、切片、maps和通道的遍歷。對於通道而言,在通道關閉時會觸發遍歷通道的代碼。能夠參照下面的代碼(下面的代碼並不能正常工做):

package main
import (
    "fmt"
)

func main() {
    message := make(chan string)
    count := 3
    
    go func() {
       for i := 1; i <= count; i ++ {
           messager <- fmt.Sptintf("message %d", i)
       } 
    }()
    
    for msg := range message {
        fmt.Println(msg)
    }
}
複製代碼

很是抱歉上面的程序並不能正常工做。我在上文也已經強調了,當通道被關閉時,range表達式纔會被觸發。所以爲了程序可以正常工做,開發者必須在程序中調用close函數。這樣gotroutine的代碼將會是下面的樣子:

go func() {
    for i := 1; i <= count; i ++ {
        message <- fmt.Sprintf("message %d", i)
    }
    close(message)
}()
複製代碼

關閉通道的函數還具備一個額外的特性 - 當調用關閉通道的函數後,而後向空通道中讀取數據或是在同一個線程中讀數據,此時並不會阻塞程序。

package main
import (
	"fmt"
)

func main() {
	done := make(chan bool)
	close(done)

	fmt.Println(<- done)  // false
	fmt.Println(<- done)  // false
}
複製代碼

這個特性也可使用在同步化goroutines的代碼中。如今讓咱們在回顧下同步goroutine的代碼:

func main() {
     done := make(chan bool)

     go func() {
          println("goroutine message")

          // We are only interested in the fact of sending itself, 
          // but not in data being sent.
          done <- true
     }()

     println("main function message")
     <-done 
} 
複製代碼

上面done通道只是爲了使得程序能夠同步執行,並不須要經過通道傳遞數據。所以咱們能夠將上面的代碼做以下修改:

func main() {
     // Data is irrelevant
     done := make(chan struct{})

     go func() {
          println("goroutine message")

          // Just send a signal "I'm done"
          close(done)
     }()

     println("main function message")
     <-done
} 
複製代碼

當開發者關閉了goroutine的通道後,並不會阻塞向通道讀數據的操做,所以main函數能夠繼續執行。

多通道和select

在真實的開發需求中,開發者每每須要面對多個goroutine和通道。獨立運行的模塊越多,越是須要高效率的同步。讓咱們來看一個更加複雜的例子:

func getMessagesChannel(msg string, delay time.Duration) <-chan string {
     c := make(chan string)
     go func() {
          for i := 1; i <= 3; i++ {
               c <- fmt.Sprintf("%s %d", msg, i)
               // Wait before sending next message
               time.Sleep(time.Millisecond * delay)
          }
     }()
     return c
}

func main() {
     c1 := getMessagesChannel("first", 300)
     c2 := getMessagesChannel("second", 150)
     c3 := getMessagesChannel("third", 10)

     for i := 1; i <= 3; i++ {
          println(<-c1)
          println(<-c2)
          println(<-c3)
     }
}
複製代碼

上面的代碼中,經過建立通道、衍生spawns的goroutine函數和休眠固定的時間後返回通道。咱們能夠明顯發現c3通道的時間間隔是最短的,所以指望先打印出c3通道中消息。然而程序的輸出倒是下面的內容:

first 1
second 1
third 1
first 2
second 2
third 2
first 3
second 3
third 3
複製代碼

顯然易見,程序的輸出是正確的。按順序的調用getMessagesChannel函數會衍生出相應goroutine和新的無緩存通道,所以只有當無緩存通道中數據被消費後,相應goroutine的代碼纔會繼續向通道中寫入數據。若是咱們指望程序的運行結果是那個goroutine更快的向通道寫入數據,將會被優先消費。

在Golang中,select關鍵詞能夠用來在通訊中處理多通道的消息傳遞。這很像其它語言的switch語法,可是select使用場景只能用來向通道中讀寫數據。所以爲了高效的處理多通道問題,能夠參照下面的代碼:

for i := 1; i <= 9; i++ {
     select {
     case msg := <-c1:
          println(msg)
	 case msg := <-c2:
          println(msg)
     case msg := <-c3:
          println(msg)
     }
}
複製代碼

請注意數字9:因爲每一個通道都會3次向通道中寫入數據,所以須要9次循環使用select。 如今咱們將會獲得指望的輸出結果,不一樣的讀寫操做間也不會相互阻塞。輸出是:

first 1
second 1
third 1 // this channel does not wait for others
third 2
third 3
second 2
first 2
second 3
first 3
複製代碼

結論

通道是Golang中很是強大也頗有趣的功能。可是若是想高效的使用這個語法特性,必須理解它的實現原理。在這篇文章中我試圖向讀者介紹關於使用通道的最基本知識點。若是你想進行深刻的研究,能夠參考下面的鏈接:

相關文章
相關標籤/搜索