Golang channel 快速入门

在这里插入图片描述

简介

Go 中多个 Goroutine 间的通信和同步一般使用 channel 来完成。channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值。

ch <- v    // 将 v 送入 channel ch
v := <-ch  // 从 ch 接收,并且赋值给 v

操作符 <- 形似箭头,其指向就是数据流的方向。

和 map 与 slice 一样,channel 使用前必须创建,未初始化的信道值为 nil。

ch := make(chan int)

默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。

创建 channel 时可以使用 <- 操作符指定信道的方向为只发送或只接收。若没有给定方向,那么该信道就是双向的。信道可通过类型转换 或 赋值被强制为只发送或只接收。

chan T          // 可以被用来发送和接收类型 T 的值
chan<- float64  // 只能被用来发送浮点数
<-chan int      // 只能被用来接收整数

缓冲 channel

channel 有两种形式的,一种是无缓冲的,一个 Go 程向这个 channel 发送了消息后,会阻塞当前Go 程,直到其他 Go 程接收了这个 channel 中的消息。

channel 可以是带缓冲的,创建 channel 时可以指定缓冲的消息数量,当消息数量小于指定值时,不会出现阻塞,超过之后才会阻塞。为 make 提供第二个参数作为缓冲大小来初始化一个缓冲 channel:

ch := make(chan int, 100)

向缓冲 channel 发送数据的时候,只有在缓冲区满的时候才会阻塞。当缓冲区为空的时候接收会阻塞。

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

运行输出:

1
2

range 和 close 操作

发送者可以 close 一个 channel 来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭:当没有值可以接收并且 channel 已经被关闭,那么经过

v, ok := <-ch

之后 ok 会被设置为 false。

循环 for v := range c 会不断地从 channel 接收值,直到它被关闭。

注意: 关闭 channel 应该由发送者而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。

还要注意: channel 与文件不同,通常情况下无需关闭它们,当一个 channel 没有被任何协程用到后最终会被 GC 回收,只有在需要告诉接收者没有更多数据的时候才有必要进行关闭,例如中断一个 range。

select 操作

select 语句使得一个 goroutine 在多个通讯操作上等待。

select 会阻塞,直到条件分支中的某个可以继续执行,这时就会执行那个条件分支。当多个都准备好的时候,会随机选择一个。

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

运行输出:

0
1
1
2
3
5
8
13
21
34
quit

为了非阻塞的发送或者接收,可使用 default 分支。当 select 中的其他条件分支都没有准备好的时候,default 分支会被执行。

package main

import (
	"fmt"
	"time"
)

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(200 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("default")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

运行输出:

default
default
tick.
default
default
tick.
BOOM!

注意要点

  • 读取已关闭的 channel,不会发生阻塞,剩余的数据仍可以取出;若没有数据,取出的值是对应类型的零值;
  • 向关闭的 channel 中写数据,无论是否带缓冲,都会引发 panic;
  • 关闭已经关闭的 channel 会导致 panic,避免重复关闭;
  • 关闭 nil channel 会引发 panic,关闭前需要确保 channel 非空;
  • Go 在语法上禁止关闭只读 channel;
  • channel 的关闭原则:不要从接收端关闭 channel,也不要在多个并发发送端中关闭 channel,而是在在唯一或最后一个发送端中关闭 channel,如此可以保证不会向一个已经关闭的 channel 发送值或者关闭一个已经关闭的 channel;
  • Go 中没有提供判断信道是否关闭的函数,因此没有办法只判断 channel 是否已经关闭而不从中取值或向其中写入。

常见用法

(1)无缓冲 channel 充当条件变量实现 Go 程同步。

c := make(chan int)  // 分配一个信道
// 在Go程中启动排序。当它完成后,在信道上发送信号。
go func() {
	list.Sort()
	c <- 1  // 发送信号,什么值无所谓。
}()
doSomethingForAWhile()
<-c   // 阻塞等待排序结束,丢弃发来的值。

(2)带缓冲的信道充当信号量,例如限制吞吐量。

var sem = make(chan int, MaxOutstanding)

func Serve(queue chan *Request) {
	for req := range queue {
		sem <- 1
		go func(req *Request) {
			process(req)
			<-sem
		}(req)
	}
}

(3)channel 充当消息队列实现消费者生产者模型。
通过 channel 可以比较方便的实现生产者消费者模型,开启一个生产者线程,一个消费者线程,生产者线程往 channel 中发送消息,同时阻塞,消费者线程阻塞等待获取 channel 中的消息,进行处理。当生产者在完成了所有的消息发送后,close channel 通知消费者线程退出。

func main() {
	ichan := make(chan int)

	// 生产者
	go func() {
		for i := 0; i < 3; i++ {
			ichan <- i
			fmt.Printf("write finish, value=%v\n", i)
		}
		close(ichan)
	}()

	// 消费者
	func() {
		for v := range ichan {
			fmt.Printf("read finish, value=%v\n", v)
		}
	}()
}

运行输出:

read finish, value=0
write finish, value=0
write finish, value=1
read finish, value=1
read finish, value=2
write finish, value=2

(4)channel 的超时处理。
利用 time 包可以实现 channel 的超时处理,当一个 channel 读取超过一定时间没有消息到来时,就可以得到超时通知处理,防止一直阻塞当前线程。

func main() {
	g, quit := make(chan int), make(chan bool)

	// 生产消息
	go func() {
		for i := 0; i < 3; i++ {
			g <- i
		}
	}()

	// 消费消息
	go func() {
		for {
			select {
			case v := <-g:
				fmt.Println(v)
			case <-time.After(time.Second * time.Duration(2)):
				quit <- true
				fmt.Println("超时,通知主线程退出")
				return
			}
		}
	}()

	// 阻塞主线程,等待消费线程结束
	<-quit
	fmt.Println("收到退出通知,主线程退出")
}

运行输出:

0
1
2
收到退出通知,主线程退出

(5)指定 channel 为输入或输出型。
创建或申明 channel 时可以在显示指定它是输入型还是输出型的,输入型则不能从中读取消息,否则编译报错,同理,输出型不能输入消息。

这样可以在编写代码时防书写错误导致程序一场。指定输入输出类型可以在方法参数时设定,那么它只在当前方法中会做输入输出限制,这样可以将错误提前暴露于编译期。

func main() {
	ch, quit := make(chan int), make(chan bool)

	// 输入型 channel 格式: inChan chan<- int,如果对其读取则编译报错
	go func(inChan chan<- int) {
		for i := 0; i < 5; i++ {
			inChan <- i
		}
		close(inChan)
	}(ch)

	// 输出型 channel 格式: inChan <-chan int,如果对其输入则编译报错
	go func(outChan <-chan int) {
		for v := range outChan {
			fmt.Printf("print out value=%v\n", v)
		}
		quit <- true
	}(ch)

	// 阻塞主线程,等待消费线程完成消费
	<-quit
	fmt.Println("收到退出通知,主线程退出")
}

输出运行:

print out value=0
print out value=1
print out value=2
print out value=3
print out value=4
收到退出通知,主线程退出

(6)使用 channel 监听指定信号。
可以创建一个 os.Signal 类型的 channel,同时通过 signal.Notify 来监听 os.Interrupt 这个中断信号,因此执行到<- quit时就会阻塞在这里,直到收到了 os.Interrupt 这个中断信号,比如按 Ctrl+C 中断程序的时候,主程序就会退出了。当然还可以监听其他信号,例如 os.Kill 等。

func main() {
	quit := make(chan os.Signal)
	signal.Notify(quit, os.Interrupt)
	fmt.Println("按 Ctrl+C 可退出程序")
	<-quit
	fmt.Println("主程序退出")
}

(7)channel 的关闭。

  • 使用 sync.once 在发送端保证只关闭一次。
type MyChannel struct {
    C    chan T
    once sync.Once
}

func NewMyChannel() *MyChannel {
    return &MyChannel{C: make(chan T)}
}

func (mc *MyChannel) SafeClose() {
    mc.once.Do(func(){
        close(mc.C)
    })
}
  • 也可以用 sync.Mutex 来避免多次关闭。
type MyChannel struct {
    C      chan T
    closed bool
    mutex  sync.Mutex
}

func NewMyChannel() *MyChannel {
    return &MyChannel{C: make(chan T)}
}

func (mc *MyChannel) SafeClose() {
    mc.mutex.Lock()
    if !mc.closed {
        close(mc.C)
        mc.closed = true
    }
    mc.mutex.Unlock()
}

func (mc *MyChannel) IsClosed() bool {
    mc.mutex.Lock()
    defer mc.mutex.Unlock()
    return mc.closed
}
  • 多次关闭,捕获 panic。
func SafeClose(ch chan T) (justClosed bool) {
    defer func() {
        if recover() != nil {
            justClosed = false
        }
    }()
    
    // assume ch != nil here
    close(ch) // panic if ch is closed
    return true
}

我们应该要理解为什么 Go 不支持内置 SafeClose 函数来关闭 channel,原因在于并不推荐从接收端或者多个并发发送端关闭 channel。Golang 甚至禁止关闭只接收的 channel。


参考文献

[1] Golang.A Tour of Go
[2] Golang.Channel types
[3] 简书.Go的channel常见使用方式
[4] 简书.如何优雅地关闭Go channel
[5] StackOverflow.How to check a channel is closed or not without reading it?

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页