天天看點

Go基礎--goroutine和channel

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,來達成處理其他輸出或者輸出時間逾時和取消的邏輯

補充

不同的goroutine之間如何通信

首先我們能夠想到的有:全局變量的方式,我們先通過這種本方法來示範:

package main

import (
    "time"
    "fmt"
)

var exits [3]bool

func calc(index int){
    for i:=0;i<1000;i++{
        time.Sleep(time.Millisecond)
    }
    exits[index] = true
}

func main() {
    start := time.Now().UnixNano()
    go calc(0)
    go calc(1)
    go calc(2)

    for{
        if exits[0] && exits[1] &&exits[2]{
            break
        }
    }
    end := time.Now().UnixNano()
    fmt.Println("finished,const:%d ms",end-start)
}      

這種方法其實比較笨,go為我們提供了鎖同步的方式 sync.WaitGroup,示範代碼為:

//等待一組goroutine執行完成

package main

import (
    "time"
    "fmt"
    "sync"
)

var waitGroup sync.WaitGroup

func calc(index int){
    for i:=0;i<1000;i++{
        time.Sleep(time.Millisecond)
    }
    //執行完成的時候Done
    waitGroup.Done()
}

func main() {
    start := time.Now().UnixNano()
    for i:=0;i<3;i++{
        // 每次在調用之前add
        waitGroup.Add(1)
        go calc(i)
    }
    //在循環外等待wait
    waitGroup.Wait()
    end := time.Now().UnixNano()
    fmt.Println("finished,const:%d ms",end-start)
}      

關于Channel的補充

channel 概念:

類似unix中的管道pipe

先進先出

線程安全,多個goroutine同時通路,不需要加鎖

channel是有類型的,一個整數的channel隻能存放整數 

定時器的補充

//定時器
package main

import (
    "time"
    "fmt"
)

func main() {
    t := time.NewTicker(time.Second)
    for v:= range t.C{
        fmt.Println("hello",v)
    }
}      
// 一次性定時器
package main

import (
    "time"
    "fmt"
)

func main() {
    select{
    case <- time.After(time.Second):
        fmt.Println("after")
    }
}      

 逾時控制

package main

import (
    "time"
    "fmt"
)

func queryDb(ch chan int){
    time.Sleep(time.Second)
    ch <- 100
}

func main() {
    ch := make(chan int)
    go queryDb(ch)
    t := time.NewTicker(time.Second*4)
    select{
    case v:=<-ch:
        fmt.Println("result:",v)
    case <-t.C:
        fmt.Println("timeout")      

關于單元測試和異常捕獲的補充

package main

import (
    "time"
    "fmt"
)

func calc(){
    // defer 定義的後面出現錯誤的都可以捕獲到
    defer func() {
        err := recover()
        if err!=nil{
            fmt.Println(err)
        }
    }()
    var p *int
    *p = 100

}      

關于單元測試的簡單例子示範:

package test

func add(a,b int)int{
    return a+b
}      

上面的代碼,如果想要做單元測試,我們需要定義一個測試檔案,我把上面代碼的檔案名是calc.go,單元測試代碼的檔案名calc_test.go

package test

import "testing"

func TestAdd(t *testing.T){
    result := add(2,3)
    if result != 5{
        t.Fatalf("add is not right")
        return
    }
    t.Logf("add is right")
}      

這樣當我們需要測試add函數的時候隻需要在該包下執行go test,效果如下:

Go基礎--goroutine和channel

如果想要看更詳細的可以通過go test -v,效果如下:

Go基礎--goroutine和channel

所有的努力都值得期許,每一份夢想都應該灌溉!