通道 - Go 語言學習筆記

前言

在 Go 語言裏,你不只可使用原子函數和互斥鎖來保證對共享資源的安全訪問以及消除競爭狀態,還可使用通道,經過發送和接收須要共享的資源,在 goroutine 之間作同步。安全

若是說goroutine是Go併發的執行體,那麼「通道」就是他們之間的鏈接。bash

簡介

通道(channel)是實現兩個 goroutine 之間通訊的機制。當一個資源須要在 goroutine 之間共享時,通道在 goroutine 之間架起了一個管道,並提供了確保同步交換數據的機制。數據結構

建立通道

建立通道時,須要指定將要被共享的數據的類型。能夠經過通道共享內置類型、命名類型、結構類型和引用類型的值或者指針。併發

在 Go 語言中通道是引用類型,使用內置函數 make 來建立一個通道
格式以下:函數

通道實例 := make(chan 數據類型)
複製代碼

make 的第一個參數須要是關鍵字 chan,以後跟着容許通道交換的數據的類型。若是建立的是一個有緩衝的通道,以後還須要在第二個參數指定這個通道的緩衝區的大小。post

  • 無緩衝的通道

無緩衝的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道。這種類型的通道要求發送 goroutine 和接收 goroutine 同時準備好,才能完成發送和接收操做。若是兩個 goroutine 沒有同時準備好,通道會致使先執行發送或接收操做的 gotoutine 阻塞等待。這種對通道進行發送和接收的交互行爲自己就是同步的。其中任意一個操做都沒法離開另外一個操做單獨存在。ui

// 無緩衝的整型通道
unbuffered := make(chan int)
複製代碼
  • 有緩衝的通道

有緩衝的通道(buffered channel)是一種在被接收前能存儲一個或者多個值的通道。這種類型的通道並不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動做的條件也會不一樣。只有在通道中沒有要接收的值時,接收動做纔會阻塞。只有在通道沒有可用緩衝區容納被髮送的值時,發送動做纔會阻塞。spa

// 有緩衝的字符chuang
buffered := make(chan string, 10)
複製代碼

注意:若是通道不帶緩衝,發送方會阻塞直到接收方從通道中接收了值。若是通道帶緩衝,發送方則會阻塞直到發送的值被拷貝到緩衝區內;若是緩衝區已滿,則意味着須要等待直到某個接收方獲取到一個值。接收方在有值能夠接收以前會一直阻塞。指針

通道傳值

操做符 <- 用於指定通道的方向,發送或接收。若是未指定方向,則爲雙向通道。
格式以下:code

通道變量 <- 值
複製代碼
  • 通道變量:經過make建立好的通道實例。
  • 值:能夠是變量、常量、表達式或者函數返回值等。值的類型必須與ch通道的元素類型一致。

示例以下:

// 向通道發送值
ch <- v     // 把 v 發送到通道 ch

// 從通道里接收值
v, ok := <-ch   // 從 ch 接收數據並把值賦給 v,若是通道接收不到數據後 ok 就爲 false
複製代碼

遍歷通道

Go 經過使用 range 函數來遍歷通道以接收通道數據。

package main
 
import "fmt"
 
func main() {
    // 咱們遍歷 queue 通道里面的兩個數據
    queue := make(chan string, 2)
    
    queue <- "one"
    queue <- "two"
    close(queue)
    
    /*
    range 函數遍歷每一個從通道接收到的數據,由於 queue 再發送完兩個
    數據以後就關閉了通道,因此這裏咱們range函數在接收到兩個數據
    以後就結束了。若是上面的queue通道不關閉,那麼 range 函數就不
    會結束,從而在接收第三個數據的時候就阻塞了。
    */

    for elem := range queue {
        fmt.Println(elem)
    }
}
複製代碼

執行輸出結果爲:

one
two
複製代碼
  • for 和 range 爲基本的數據結構提供了迭代功能,一樣能夠用於通道的遍歷
  • 以上例子是遍歷通道 queue 中的兩個值
  • 咱們 close 了這個通道,因此遍歷完這兩個值後結束,若是不 close的 話,將一直阻塞執行,等待接收第三個值
  • 這個例子代表,非空的通道也是能夠被關閉的,可是通道中剩下的值仍然能夠被接收到

關閉通道

通道是一個引用類型,在沒有任何外部引用時,Go 程序在運行時(runtime)會自動對通道進行垃圾回收,並且通道也能夠被主動關閉。

使用 close() 來關閉一個通道
格式:

close(ch)
複製代碼

1. 給被關閉通道發送數據將會觸發panic

被關閉的通道不會被設置爲 nil,若是嘗試對已經關閉的通道進行發送,將會觸發宕機。
示例以下:

package main
import "fmt"
func main() {
    // 建立一個整型的通道
    ch := make(chan int)
    // 關閉通道
    close(ch)
    // 打印通道的指針, 容量和長度
    fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch))
    // 給關閉的通道發送數據
    ch <- 1
}
複製代碼

代碼運行後觸發宕機:

panic: send on closed channel
複製代碼

2. 從已關閉的通道接收數據時將不會發生阻塞

從已經關閉的通道接收數據或者正在接收數據時,將會接收到通道類型的零值,而後中止阻塞並返回。

示例以下:

package main
import "fmt"
func main() {
    // 建立一個整型帶兩個緩衝的通道
    ch := make(chan int, 2)
   
    // 給通道放入兩個數據
    ch <- 0
    ch <- 1
   
    // 關閉緩衝
    close(ch)
    // 遍歷緩衝全部數據, 且多遍歷1個
    for i := 0; i < cap(ch)+1; i++ {
   
        // 從通道中取出數據
        v, ok := <-ch
       
        // 打印取出數據的狀態
        fmt.Println(v, ok)
    }
}
複製代碼

代碼運行結果以下:

0 true
1 true
0 false
複製代碼

以上運行結果的前兩行正確輸出帶緩衝通道的數據,代表緩衝通道在關閉後依然能夠訪問內部的數據。
以上運行結果的第三行的「0 false」表示通道在關閉狀態下取出的值。0 表示這個通道的默認值,false 表示沒有獲取成功,由於此時通道已經空了。咱們發現,在通道關閉後,即使通道沒有數據,在獲取時也不會發生阻塞,但此時取出數據會失敗。

使用通道示例

  • 無緩衝的通道

無緩衝的通道的一個重要做用就是在兩 goroutine 之間同步交互數據。

在網球比賽中,兩位選手會把球在兩我的之間來回傳遞。選手老是處在如下兩種狀態之一:要麼在等待接球,要麼將球打向對方。可使用兩個 goroutine 來模擬網球比賽,並使用無緩衝的通道來模擬球的來回。以下:

// 這個示例程序展現如何用無緩衝的通道來模擬2個 goroutine 間的網球比賽
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

// wg 用來等待程序結束
var wg sync.WaitGroup

func init() {
	rand.Seed(time.Now().UnixNano())
}

// main 是全部 Go 程序的入口
func main() {
	// 建立一個無緩衝的通道
	court := make(chan int)

	// 計數加2,表示要等待兩個 goroutine
	wg.Add(2)

	// 啓動兩個選手
	go player("張三", court)
	go player("李四", court)

	// 發球
	court <- 1

	// 等待遊戲結束
	wg.Wait()
}

// Player 模擬一個選手在打網球
func player(name string, court chan int)  {
	// 在函數退出時調用 Done 來通知 main 函數工做已經完成
	defer wg.Done()

	for {
		// 等待球被擊打過來
		ball, ok := <-court
		if !ok {
			// 若是通道被關閉,咱們就贏了
			fmt.Printf("球員 %s 贏了\n", name)
			return
		}

		// 選隨機數,而後用這個數來判斷咱們是否丟球
		n := rand.Intn(100)
		if n%3 == 0 {
			fmt.Printf("球員 %s 輸了\n", name)

			// 關閉通道,表示咱們輸了
			close(court)
			return
		}

		// 顯示擊球數,並將擊球數加1
		fmt.Printf("球員 %s 擊中 %d\n", name, ball)
		ball ++

		// 將球打向對手
		court <- ball
	}

}
複製代碼

執行後隨機獲得如下輸出:

球員 李四 擊中 1
球員 張三 擊中 2
球員 李四 輸了
球員 張三 贏了
複製代碼
  • 有緩衝的通道

// 這個示例程序展現如何使用有緩衝的通道和固定數目的 goroutine 來處理一堆工做
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

const (
	numberGoroutines = 4 // 要使用的 goroutine 的數量
	taskLoad = 10 // 要處理的工做的數量
)

// wg 用來等待程序完成
var wg sync.WaitGroup

// init 初始化包,Go 語言運行時會在其它代碼執行以前優先執行這個函數
func init() {
	// 初始化隨機數種子
	rand.Seed(time.Now().Unix())
}

// main 是全部 Go 程序的入口
func main() {
	// 建立一個有緩衝的通道來管理工做
	tasks := make(chan string, taskLoad)

	// 啓動 goroutine 來處理工做
	wg.Add(numberGoroutines)
	for gr := 1; gr <= numberGoroutines; gr++ {
		go worker(tasks, gr)
	}

	// 增長一組要完成的工做
	for post := 1; post <= taskLoad; post++ {
		tasks <- fmt.Sprintf("Task: %d", post)
	}

	// 當全部工做都處理完時關閉通道,以便全部 goroutine 退出
	close(tasks)

	// 等待全部工做完成
	wg.Wait()
}

// worker 做爲 goroutine 啓動來處理
// 從有緩衝的通道傳入的工做
func worker(tasks chan string, worker int)  {
	// 通知函數已經返回
	defer wg.Done()

	for {
		// 等待分配工做
		task, ok := <-tasks
		if !ok {
			// 這意味着通道已經完了,而且已被關閉
			fmt.Printf("Worker %d : Shutting Down\n", worker)
			return
		}


		// 顯示咱們開始工做了
		fmt.Printf("Worker: %d : Started %s\n", worker, task)

		// 隨機等一段時間來模擬工做
		sleep := rand.Int63n(100)
		time.Sleep(time.Duration(sleep) * time.Millisecond)

		// 顯示咱們完成工做了
		fmt.Printf("Worker: %d : Completed %s\n", worker, task)

	}

}

複製代碼

執行後隨機獲得如下輸出:

Worker: 1 : Started Task: 2
Worker: 3 : Started Task: 1
Worker: 4 : Started Task: 3
Worker: 2 : Started Task: 4
Worker: 4 : Completed Task: 3
Worker: 4 : Started Task: 5
Worker: 3 : Completed Task: 1
Worker: 3 : Started Task: 6
Worker: 4 : Completed Task: 5
Worker: 4 : Started Task: 7
Worker: 1 : Completed Task: 2
Worker: 1 : Started Task: 8
Worker: 2 : Completed Task: 4
Worker: 2 : Started Task: 9
Worker: 2 : Completed Task: 9
Worker: 2 : Started Task: 10
Worker: 3 : Completed Task: 6
Worker 3 : Shutting Down
Worker: 4 : Completed Task: 7
Worker 4 : Shutting Down
Worker: 2 : Completed Task: 10
Worker 2 : Shutting Down
Worker: 1 : Completed Task: 8
Worker 1 : Shutting Down
複製代碼

因爲程序和 Go 語言的調度器帶有隨機成分,這個程序每次執行獲得的輸出會不同。不過,經過有緩衝的通道,使用全部 4 個 goroutine 來完成工做,這個流程不變。從輸出能夠看到每一個 goroutine 是如何接收從通道里分發的工做。

相關文章
相關標籤/搜索