1. 程式人生 > >Go基礎--goroutine和channel

Go基礎--goroutine和channel

讓我 tin 能夠 pan 函數 並行 端口 pre 理解

goroutine

在go語言中,每一個並發的執行單元叫做一個goroutine

這裏說到並發,所以先解釋一下並發和並行的概念:

並發:邏輯上具備同時處理多個任務的能力

並行:物理上在同一時刻執行多個並發任務

當一個程序啟動時,其主函數即在一個單獨的goroutine中運行,一般這個goroutine是主goroutine

如果想要創建新的goroutine,只需要再執行普通函數或者方法的的前面加上關鍵字go

通過下面一個例子演示並發的效果,主goroutine會計算第45個斐波那契函數,在計算的同時會循環打印:-\|/

這裏需要知道:當主goroutine結束之後,所有的goroutine都會被打斷,程序就會退出

package main

import (
    "time"
    "fmt"
)

func spinner(delay time.Duration){
    for {
        for _,r := range `-\|/`{
            fmt.Printf("\r%c",r)
            time.Sleep(delay)
        }
    }
}

func fib(x int) int{
    // 斐波那契函數
    if x < 2{
        return x
    }
    return fib(x
-1) + fib(x-2) } func main() { go spinner(100*time.Millisecond) //開啟一個goroutine const n = 45 fibN:= fib(n) fmt.Printf("\rFib(%d) = %d\n",n,fibN) }

當第一次看到go的並發,感覺真是太好用了!!!!

所以在網絡編程裏,服務端都是需要同時可以處理很多個連接,我們看一下下面的服務端和客戶端例子

服務端:

package main

import (
    "net"
    "io"
    "time"
    "log"
)

func handleConn(c net.Conn){
    defer c.Close()
    for{
        _,err :
= io.WriteString(c,time.Now().Format("15:04:05\r\n")) if err != nil{ return } time.Sleep(1*time.Second) } } func main() { // 監聽本地tcp的8000端口 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) } }

客戶端:

package main

import (
    "io"
    "log"
    "net"
    "os"
)

func mustCopy(dst io.Writer,src io.Reader){
    // 從連接中讀取內容,並寫到標準輸出
    if _,err := io.Copy(dst,src);err !=nil{
        log.Fatal(err)
    }

}

func main(){
    conn,err := net.Dial("tcp","localhost:8000")
    if err != nil{
        log.Fatal(err)
    }
    defer conn.Close()
    mustCopy(os.Stdout, conn)
}

Channel

channel是不同的goroutine之間的通信機制。

一個goroutine通過channel給另外一個goroutine發送信息。

每個channel 都有一個特殊的類型,也就是channel可以發送的數據的類型

我們可以通過make創建一個channel如:

ch := make(chan int) 這就是創建了一個類型為int的channel

默認我們這樣創建的是無緩存的channel,當然我們可以通過第二個參數來設置容量

ch := make(chan int,10)

註意:channel是引用類型,channel的零值也是nil

兩個相同類型的channel可以使用==運算符比較。如果兩個channel引用的是相通的對象,那麽比較的結
果為真。一個channel也可以和nil進行比較。

因為channel是在不同的goroutine之間進行通信的,所以channel這裏有兩種操作:存數據和取數據,而這裏兩種操作的

方法都是通過運算符:<-

ch <- x 這裏是發送一個值x到channel中

x = <- ch 這裏是從channel獲取一個值存到變量x

<-ch 這裏是從channel中取出數據,但是不使用結果

close(ch) 用於關閉channel

當我們關閉channel後,再次發送就會導致panic異常,但是如果之前發送過數據,我們在關閉channel之後依然可以執行接收操作

如果沒有數據的話,會產生一個零值

基於channel發送消息有兩個重要方面,首先每個消息都有一個值,但是有時候通訊的事件和發送的時刻也同樣重要。

我們更希望強調通訊發送的時刻時,我們將它稱為消息事件。有些消息並不攜帶額外的信息,它僅僅是用做兩個goroutine之間的同步,這個時候我們可以用struct{}空結構體作為channel元素的類型

無緩存的channel

基於無緩存的channel的發送和接受操作將導致兩個goroutine做一次同步操作,所以無緩存channel有時候也被稱為同步channel

串聯的channel (Pipeline)

channel也可以用於多個goroutine連接在一起,一個channel的輸出作為下一個channel的輸入,這種串聯的channel就是所謂的pipeline

通過下面例子理解,第一個goroutine是一個計算器,用於生成0,1,2...形式的整數序列,然後通過channel將該整數序列

發送給第二個goroutine;第二個goroutine是一個求平方的程序,對收到的每個整數求平方,然後將平方後的結果通過第二個channel發送給第三個goroutine

第三個goroutine是一個打印程序,打印收到的每個整數

package main

import (
    "fmt"
)

func main(){
    naturals := make(chan int)
    squares := make(chan int)

    go func(){
        for x:=0;;x++{
            naturals <- x
        }
    }()

    go func(){
        for {
            x := <- naturals
            squares <- x * x
        }
    }()

    for{
        fmt.Println(<-squares)
    }
}

但是如果我把第一個生成數的寫成一個有範圍的循環,這個時候程序其實會報錯的

所以就需要想辦法讓發送知道沒有可以發給channel的數據了,也讓接受者知道沒有可以接受的數據了

這個時候就需要用到close(chan)

當一個channel被關閉後,再向該channel發送數據就會導致panic異常

當從一個已經關閉的channel中接受數據,在接收完之前發送的數據後,並不會阻塞,而會立刻返回零值,所以在從channel裏接受數據的時候可以多獲取一個值如:

go func(){
for {
x ,ok := <-naturals
if !ok{
break
}
squares <- x*x
}
close(squares)
}()

第二位ok是一個布爾值,true表示成功從channel接受到值,false表示channel已經被關閉並且裏面沒有值可以接收

單方向的channel

當一個channel座位一個函數的參數時,它一般總是被專門用於只發送或者只接收

chan <- int :表示一個只發送int的channel,只能發送不能接收

< chan int : 表示一個只接受int的channel,只能接收不能發送

當然在有時候我們需要獲取channel內部緩存的容量,可以通過內置的cap函數獲取

而len函數則返回的是channel內實際有效的元素個數

基於select的多路復用

這裏先說一個擁有的知識點:time.Tick函數

這個函數返回一個channel,通過下面代碼進行理解:

package main

import (
    "time"
    "fmt"
)

func main() {
    tick := time.Tick(1*time.Second)
    for countdown :=10;countdown>0;countdown--{
        j :=<- tick
        fmt.Println(j)
    }
}

程序會循環打印一個時間戳

select 語句:

select {
 case <-ch1:
     // ...
 case x := <-ch2:
     // ...use x...
 case ch3 <- y:
     // ...
 default:
    // ... 
}

select語句的形式其實和switch語句有點類似,這裏每個case代表一個通信操作

在某個channel上發送或者接收,並且會包含一些語句組成的一個語句塊 。

select中的default來設置當 其它的操作都不能夠馬上被處理時程序需要執行哪些邏輯

channel 的零值是nil, 並且對nil的channel 發送或者接收操作都會永遠阻塞,在select語句中操作nil的channel永遠都不會被select到。

這可以讓我們用nil來激活或者禁用case,來達成處理其他輸出或者輸出時間超時和取消的邏輯

Go基礎--goroutine和channel