Golang 併發程式設計(channel實現)

youou發表於2021-09-09

上期我們使用 sync 實現方式支援協程(goroutine)併發。

接下來繼續使用下載檔案例子,使用 channel 實現併發

package main

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

func main()  {
	// ============== channel  ==============
	startTime2 := time.Now()

	testChannel()

	endTime2 := time.Now()
	fmt.Printf("======> Done! use time: %f",(endTime2.Sub(startTime2).Seconds()))
	fmt.Println()
}

// 使用 channel 通道,可以在協程之間傳遞訊息。等待併發協程返回訊息。
var ch = make(chan string, 10) // 建立大小 10 的快取通道
func testChannel() {
	for i := 0; i  3; i++ {
		go downloadV2("a.com/" + string(i+'0'))
	}
	for i := 0; i  3; i++ {
		msg := ch // 等待通道返回訊息。
		fmt.Println("finish", msg)
	}
}

// ================= 工具 V  =================

func downloadV2(url string) {
	fmt.Println("start to download", url)
	time.Sleep(time.Second)
	ch  url // 將 url 傳送給通道
}

channel (通道)

channel 就是 goroutine 之間的通訊機制。每個 channel 都有一個特殊的型別,也就是 channels 可傳送資料的型別。一個可以傳送 int 型別資料的 channel 一般寫為 chan int。

Golang 提倡使用 通訊的方法代替共享記憶體,當一個資源需要在 goroutine 之間共享時,通道在 goroutine 之間架起了一個管道,並提供了確保同步交換資料的機制。

宣告通道時,需要指定被共享的資料的型別。可以透過通道共享內建型別、命名型別、結構型別和引用型別的值或者指標。

宣告通道型別

宣告通道時,需要指定被共享的資料的型別,宣告channel語法如下:var 通道變數 chan 通道型別

  • 通道型別:通道內傳輸的資料的型別
  • chan 型別的空值是 nil,宣告後需要配合 make 後才能使用。

建立通道

通道是引用型別,需要使用 make 進行建立,格式如下:
通道例項 := make(chan 資料型別)

資料型別:通道內被傳輸的資料的型別
通道例項:透過make建立的通道控制程式碼。

舉個例子:

ch1 := make(chan int)                 // 建立一個整型型別的通道
ch2 := make(chan interface{})         // 建立一個空介面型別的通道, 可以存放任意格式

type Equip struct{ /* 一些欄位 */ }
ch2 := make(chan *Equip)             // 建立Equip指標型別的通道, 可以存放*Equip

使用通道傳送資料

通道建立後,就可以使用通道進行傳送和接收操作。

通道傳送資料的格式

通道的傳送使用特殊的運算子,將資料透過通道傳送的格式為:
通道變數

  • 通道變數:透過make建立好的通道例項。
  • 值:可以是變數、常量、表示式或者函式返回值等。值的型別必須與ch通道的元素型別一致

使用 make 建立一個通道後,就可以使用

// 建立一個空介面通道
ch := make(chan interface{})
// 將0放入通道中
ch  0
// 將hello字串放入通道中
ch  "hello"

傳送將持續阻塞直到資料被接收

把資料往通道中傳送時,如果接收方一直沒接收,那麼傳送操作將持續阻塞
Go 程式執行時能智慧地發現一些永遠無法傳送成功的語句並做出提示,程式碼如下:

package main
func main() {
    // 建立一個整型通道
    ch := make(chan int)
    // 嘗試將0透過通道傳送
    ch  0
}

執行程式碼,報錯:

fatal error: all goroutines are asleep - deadlock!

報錯的意思是:執行時發現所有的 goroutine(包括main)都處於等待 goroutine。
也就是說所有 goroutine 中的 channel 並沒有形成傳送和接收對應的程式碼。

使用通道接收資料

通道接收同樣使用運算子,通道接收有如下特性:

  • 通道的收發操作在不同的兩個 goroutine 間進行。
    由於通道的資料在沒有接收方處理時,資料傳送方會持續阻塞,因此通道的接收必定在另外一個 goroutine 中進行。

  • 接收將持續阻塞直到傳送方傳送資料。
    如果接收方接收時,通道中沒有傳送方傳送資料,接收方也會發生阻塞,直到傳送方傳送資料為止。

  • 每次接收一個元素。
    通道一次只能接收一個資料元素。

通道的資料接收一共有以下 4 種寫法:

阻塞接收資料

阻塞模式接收資料時,將接收變數作為data :=

執行該語句時將會阻塞,直到接收到資料並賦值給 data 變數。

非阻塞接收資料

使用非阻塞方式從通道接收資料時,語句不會發生阻塞,格式如下:
data, ok :=

  • data:表示接收到的資料。未接收到資料時,data 為通道型別的零值。
  • ok:表示是否接收到資料。

非阻塞的通道接收方法可能造成更高 CPU 佔用,因此使用非常少。如果需要實現接收超時檢測,可以配合 select 和計數 channel 進行,可以參見後面的內容。

接收任意資料,忽略接收的資料

阻塞接收資料後,忽略從通道返回的資料,格式如下:

執行該語句時將會發生阻塞,直到接收到資料,但接收到的資料會被忽略。這個方式實際上只是透過通道在 goroutine 間阻塞收發實現併發同步。

使用通道做併發同步的寫法,可以參考下面的例子:

package main
import (
    "fmt"
)
func main() {
    // 構建一個通道
    ch := make(chan int)
    // 開啟一個併發匿名函式
    go func() {
        fmt.Println("start goroutine")
        // 透過通道通知main的goroutine
        ch  0
        fmt.Println("exit goroutine")
    }()
    fmt.Println("wait goroutine")
    // 等待匿名goroutine
    ch
    fmt.Println("all done")
}

// 輸出如下:
// wait goroutine
// start goroutine
// exit goroutine
// all done

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4729/viewspace-2797138/,如需轉載,請註明出處,否則將追究法律責任。

相關文章