通道 - 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 是如何接收从通道里分发的工做。

相关文章
相关标签/搜索