赞
踩
channel 提供了一种通信机制,通过发送和接收指定元素类型的值来完成并发执行函数间的通信。
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType
操作符 <- 形似箭头,其指向是数据流的方向。如果在定义 channel 时不指定,则说明 channel 是双向的。信道可通过类型转换或赋值被强制为只发送或只接收。
chan T // 可以被用来发送和接收类型 T 的值
chan<- float64 // 只能被用来发送浮点数
<-chan int // 只能被用来接收整数
通过操作符 <- 向其发送或从其接收值。
ch <- v // 将 v 送入 ch
v := <-ch // 从 ch 接收,并且赋值给 v
v, ok := <-ch // 成功取出 ok 为 true,如果 ch 已关闭,则 ok 为 false,v 为对应类型的零值
和 map、slice 一样,channel 使用前必须创建,未初始化的信道值为 nil。
// 未初始化的 channel 为 nil
var ch chan int
// 创建 channel
ch := make(chan int)
默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。
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
channel 与 array 和 slice 相同,可以使用内建函数 len 与 cap 分别获取长度和容量。长度表示 channel 中的元素个数,容量就是带缓冲 channel 的容量。
ch1 := make(chan int)
fmt.Println(len(ch1), cap(ch1)) // 0 0
ch2 := make(chan int, 10)
ch2 <- 0
fmt.Println(len(ch2), cap(ch2))// 1 10
发送者可以 close 一个 channel 来表示没有值会被发送了。接收者可以通过赋值语句的第二参数来测试 channel 是否被关闭。当没有值可以接收并且 channel 已经被关闭,那么经过
v, ok := <-ch
之后 ok 会被设置为 false,v 为对应类型的零值。
循环 for v := range c
会不断地从 channel 接收值,直到它被关闭。
注意: 关闭 channel 应该由发送者而不是接收者。向一个已经关闭的 channel 发送数据会引起 panic。
还要注意: channel 与文件不同,通常情况下无需关闭它们,当一个 channel 没有被任何协程用到后最终会被 GC 回收,只有在需要告诉接收者没有更多数据的时候才有必要进行关闭,例如中断一个 range。
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 的三种状态不同操作有不同的结果:
操作 | 未关闭 | 已关闭 | nil |
---|---|---|---|
发数据 | 阻塞或成功发送 | panic | 永久阻塞 |
取数据 | 阻塞或成功接收 | 成功接收或零值 | 永久阻塞 |
关闭 | 成功关闭 | panic | panic |
从上面的表格得知,从 nil channel 中读取或写入会永久阻塞。读取不论形式,包括 comma ok 式也会阻塞。
// 写入永久阻塞
var ch chan string
ch <- "Go"
// 读取永久阻塞
x := <-ch
x, ok := <-ch
for x := range ch {
...
}
另外还需要知道:
(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 的关闭。
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)
})
}
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 }
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。
Golang.A Tour of Go
Golang.Channel types
The Go Programming Language Specification.Receive operator
简书.Go的channel常见使用方式
简书.如何优雅地关闭Go channel
StackOverflow.How to check a channel is closed or not without reading it?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。