1. 程式人生 > 其它 >Go語言學習之路-12-併發(2)-channel

Go語言學習之路-12-併發(2)-channel

目錄

channel

什麼是channel

channel就是一個先進先出的佇列,就是一種排隊的機制通道,分解它的屬性:

  • 排隊
  • 阻塞(通道滿了就等等著,好比去火車站排隊一樣,沒有人檢票了就的在檢票口等著)
  • 先進先出

CSP(Communicating Sequential Processes)通訊實現資料共享

Go語言CSP理念是:通過通訊共享資料,而不是通過共享記憶體共享資料

channel的作用就是實現CSP理念的基石

goroutine和channel使用

goroutine和channel分別解決的什麼問題

  • goroutine解決了併發的問題
  • channel解決了併發通訊的問題,並通過CSP理念解決了併發goroutine的資料共享

channel的基本使用

建立通道

channel是一種型別並且是一種引用型別。所以在建立channel的時候需要make

make(chan 元素型別, [緩衝佇列長度])

// 如果不設定緩佇列的長度,就是無緩衝佇列
ch1 := make(chan int)
// 如果設定了緩衝佇列的長度,就是有緩衝佇列,他們有什麼不同我們下面繼續看

給channel傳送和接收訊息

channel有三種操作:

  • 傳送
  • 接收
  • 關閉

傳送和接收都是通過: <-

傳送(給通道發訊息)

// 給通道傳送一個訊息
ch1 <- 1

接收(從通道接受訊息)

// 接收一個訊息並賦值
v1 := <-ch1
// 接收一個訊息並忽略這個值
<-ch1

關閉channel

close關閉channel

// 關閉channel
close(ch1)

判斷channel是否關閉

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int, 10)

	ch1 <- 1
	ch1 <- 2
	ch1 <- 3

	close(ch1)

	for {
		i,ok := <- ch1

		if ok {
			fmt.Printf("channel是正常的,獲取的值是:%v\n",i)
		}else {
			fmt.Printf("channel是關閉的, 需要退出\n")
			break
		}
		time.Sleep(time.Second)
	}
}

for range從通道迴圈取值

雖然一個通道關閉後在往裡面寫入元素會產生panic,但是channel關閉後可以繼續讀裡面的元素,通過for range讀完就可以正常推出

注意:如果這個通道沒有關閉使用for range會產生panic,因為如果通道沒有關閉的話會一直卡著死鎖~

package main

import "fmt"

func main() {
	ch1 := make(chan int, 100)
	ch2 := make(chan int, 100)

	// 1 迴圈往ch1裡寫資料
	go func(){
		for i := 1; i < 101; i++ {
			ch1 <- i
		}
		// 這裡注意用完ch1, 這裡要關閉ch1
		close(ch1)
	}()

	// 2 迴圈從ch1裡取資料,並求平方然後寫入到ch2去
	go func() {
		for j := range ch1{
			ch2 <- j * j
		}
		// 當資料寫完後,注意這裡要關閉ch2
		close(ch2)
	}()

	// 3 迴圈取結果
	for r := range ch2 {
		fmt.Println(r)
	}
}

通道(channel)的例子

無緩衝通道

看實際的例子:

package main

import "fmt"

func main() {
       // 如果不設定緩佇列的長度,就是無緩衝佇列
	ch1 := make(chan int)
	// 接收一個訊息並賦值
	v1 := <-ch1
	// 接收一個訊息並忽略這個值
	fmt.Println(v1)
	// 關閉channel
	close(ch1)
}

但是會出現異常(死鎖): fatal error: all goroutines are asleep - deadlock!

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/Users/apple/code/2del/g1/main.go:9 +0x73

無緩衝通道死鎖原因

**無緩衝的通道只有在有人接收值的時候才能傳送值**

舉個栗子: 你去銀行辦理業務,你去辦理業務了但是沒有業務員沒人幫你幹活只能乾著急,一樣的道理,要給無緩衝通道發訊息必須有接收方?--> 啟動一個goroutine接收訊息

package main

import (
	"fmt"
)

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 啟用goroutine從通道接收值
	ch <- 100
	fmt.Println("傳送成功")
}
  1. 無緩衝通道,傳送會阻塞,直到另一個goroutine開始接收channel裡的資料(你去辦理銀行辦理業務,他們銀行只有一個業務員,但是當前沒有上班,所以只能阻塞(等著),直到業務員開始辦理業務了才可以)
  2. 使用無緩衝通道導致傳送接收同步化(一個發一個收),就成為了序列的同步的情況了,所以無緩衝通道又稱為:同步通道

有緩衝通道

解決:無緩衝通道帶來的同步問題,可以使用有緩衝通道,讓訊息可以進行非同步處理~

只要channel的容量大於0,就是無緩衝通道(現在你這個銀行[channel]有多個業務員了可以同時的處理問題了,就1個業務員的時候只能序列來,多個就可以並行了)

package main

import (
	"fmt"
	"time"
)

func test(ch chan int)  {
    for i := 0; i < 100; i++ {
        ch <- i
    }
    close(ch)
}

func main()  {
    start := time.Now()
    ch := make(chan int, 100)
	// 有緩衝通道: 程式執行耗時(s): 0.000247284
	// 無緩衝通道: 程式執行耗時(s): 0.000391499

    go test(ch)
    for i := range ch {
        fmt.Println("接收到的資料:", i)
    }
    end := time.Now()
    consume := end.Sub(start).Seconds()
    fmt.Println("程式執行耗時(s):", consume)
}

無緩channel和有緩衝channel使用場景

首先這裡沒必要想,我什麼情況下必須用什麼channel,需要看場景:用於同步的時候,一般用無緩衝 , 一般大量交換資料、或者併發處理(生產-消費)情況下用快取的多

無緩衝的例子:

** 一個函式開啟一個goroutine處理一個事情,然後函式繼續執行,但執行到某一步後,需要等前面的那個goroutine的結果**

package main

import (
	"fmt"
)

func main() {

	// 現在有一個任務
	// 它需要並行準備兩份資料
	Worker()

}

// 斐波那契數列計算
func Fib(n int) int {
	if n == 1 || n == 2 {
		return 1
	}
	return (Fib(n-2) + Fib(n-1))
}



// 比如我有一個函式 Worker
// 這個函式的功能是
// 1 併發一個goroutine處理一個事情,不阻塞後面的邏輯
// 2 但是執行到某一步需要等待上面的goroutine結束

func Worker(){
	done := make(chan int)

	// 1 併發一個goroutine處理一個事情(我去拿一份資料,但是這份資料不立刻就用但是比較耗時間)
	// 這個時候就可以啟動一個goroutine先去取資料
	go func(){
		done <- Fib(10)
		// 用完的關閉否則只有接收方了就會出現死鎖的問題
		close(done)
	}()

	// 呼叫一個標準的業務邏輯
	n2 := func()int{return 1 + 1}()
	fmt.Printf("n2的結果是:%v, 這裡很快就執行完了\n",n2)

	// 2 但是執行到某一步需要等待上面的goroutine結束
	// 等待非同步任務結束然後在計算結果
	fmt.Printf("這裡需要等待非同步的流程執行完\n")
	n1 := <-done
	fmt.Printf("n1的結果是:%v\n",n2)

	sum := n1 + n2

	fmt.Printf("n1 和 n2的結果是:%v",sum)
}

有緩衝例子

無緩衝需要同步,有緩衝就是需要非同步操作,它的場景是併發,且可以限制併發數量

有緩衝通道不需要收發同時都準備好

正常情況下我們是可以無限開啟gorotuine的但是有時候我們既要併發也要限制這個時候緩衝通道就比較合適了~

package main

import (
	"fmt"
	"time"
)

func main() {
	// 假如我有1000個任務
	// 沒開啟一個goroutine就往channel裡寫入一個元素,直到寫滿了就阻塞了
	// 當任務執行完從channel裡哪出一個元素,for迴圈繼續

    control := make(chan interface{},2)

    for i := 1; i <= 10; i++ {
		// 這裡應該放上面,如果放下面就會每次都執行三個了
		// 往有緩衝channel裡寫入一個元素
        control <- i
        go func(num int) {
			// 虛擬碼邏輯
			fmt.Printf("當前時間是:%v\n",time.Now().Format("2006-01-02 15:04:05"))
            time.Sleep(time.Second * 2)
			// 從channel裡接收資料
            <-control
        }(i)
    }
}

單向通道

有時候我們會將通道作為引數傳遞,通道是雙向的:可以讀也可以寫,如果不做限制很容易用亂產生死鎖,所以要限制它:只讀或者只寫,所以go提供了單向通道來解決這個case

goroutine 池例子

package main

import (
	"fmt"
	"time"
)

func main() {
	jobs := make(chan int, 100)
	rets := make(chan int, 100)

	// 啟動2個goroutine
	for i := 1; i < 3; i++ {
		go worker(i, jobs, rets)
	}

	// 總共10個任務
	for j := 1; j < 11; j++ {
		jobs <- j
	}

	// 關閉jobs的channel
	// 讓goroutine的for迴圈正常退出
	close(jobs)

	// 迴圈讀取結果
	for r := 1; r < 11; r++ {
		fmt.Println("結果是:",<-rets)
	}
}

// chanJobs 只讀
// chanRets 只寫
func worker(i int, chanJobs <-chan int, chanRets chan<- int) {
	for job := range chanJobs {
		fmt.Printf("開始:當前goroutine的ID是:%v, 當前job的ID是:%v\n",i,job)
		time.Sleep(time.Second)
		fmt.Printf("開始:當前goroutine的ID是:%v, 當前job的ID是:%v\n",i,job)
		chanRets <- job * 2
	}
}

channel總結

正常的channel

接收:
	空的:
		阻塞
	沒空:
		接收
	通道已關閉:
		1 如果通道里還有元素會繼續讀取
		2 如果通道里已經沒有元素了,也會取出值只是他是這個通道元素的零值

傳送:
	滿了:
		阻塞
	沒滿:
		傳送
	通道已關閉:
		panic異常
關閉:
	通道未關閉:
		正常關閉
	通道已關閉:
		panic異常

對nil型別的channel操作

只申明瞭但是沒有初始化的channel

傳送:
      阻塞
接收:
      阻塞
關閉:
      panic
package main

import (
	"fmt"
	"time"
)

func main() {
	// 因為這個ch變數只是聲明瞭但是並沒有初始化
	var ch chan int

	go send(ch)
	<-ch
	time.Sleep(time.Second * 1)
}

func send(ch chan int) {
	fmt.Println("Sending value to channnel start")
	ch <- 1
	fmt.Println("Sending value to channnel finish")
}

select多路複用

在某些場景下有時候回需要同時從多個通道接收資料。通道在接收資料時,如果沒有資料可以接收將會發生阻塞。為了解決這個問題go為我們提供了select關鍵字

golang中的select語句格式如下

	select {
	case v1 := <-ch1:
		// 如果從 ch1 通道成功接收資料,則執行該分支程式碼
		fmt.Println("接收成功", v1)
	case ch2 <- 1:
		// 如果成功向 ch2 通道成功傳送資料,則執行該分支程式碼
		fmt.Println("傳送成功")
	default:
		// 如果上面都沒有成功,則進入 default 分支處理流程
		fmt.Println("走的default分支")
	}

select的語法結構有點類似於switch,但select裡的case後面並不帶判斷條件,而是一個通道的操作,所有case中的表示式都必須是channel的傳送或接收操作

另外:golang 的 select 就是監聽 IO 操作,當 IO 操作發生時,觸發相應的動作每個case語句裡必須是一個IO操作,確切的說,應該是一個面向channel的IO操作。

Go 語言的 select 語句借鑑自 Unix 的 select() 函式,在 Unix 中,可以通過呼叫 select() 函式來監控一系列的檔案控制代碼,一旦其中一個檔案控制代碼發生了 IO 動作,該 select() 呼叫就會被返回(C 語言中就是這麼做的),後來該機制也被用於實現高併發的 Socket 伺服器程式。Go 語言直接在語言級別支援 select關鍵字,用於處理併發程式設計中通道之間非同步 IO 通訊問題。

作者:羅天帥
出處:http://www.cnblogs.com/luotianshuai/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線。