本文适合初学者阅读
通道
- 从底层实现上来说, 通道只是一个队列, 分为同步和异步两种模式
- 同步模式,发送和接收双方配对, 然后直接复制数据给对方, 若配对失败, 则置入等待队列, 直到另一方出现后才被唤醒.
- 异步模式, 抢夺的则是数据缓冲槽, 发送方要求有空槽可供写入, 接收方则要求有缓冲数据可读. 需求不符时, 同样加入等待队列, 直到有另一方写入数据或腾出空槽后被唤醒.
- 通道除传递数据外, 还常被用作事件通知
package main
func main() {
done := make(chan struct{}) // 结束事件
c := make(chan string) // 数据传输通道
go func() {
s := <-c // s接收来自c的数据
println(s)
close(done) // 关闭通道, 作为结束通知
}()
c <- "hi" // 发送消息
<-done // 阻塞, 直到有数据或管道关闭
}
- 同步模式必须有配对操作的goruntine出现, 否则会一直阻塞, 而异步模式在缓冲区未满或数据未读完前, 不会阻塞.
package main
func main() {
c := make(chan string, 3) //创建带3个缓冲槽的异步通道
c <- "hi" // 缓冲区未满, 不会阻塞
c <- "word"
println(<-c) // 缓冲区尚有数据, 不会阻塞
println(<-c)
println(<-c) // 缓冲区没有数据了, 产生死锁.
}
//hi
//word
//fatal error: all goroutines are asleep - deadlock!
//goroutine 1 [chan receive]:
- 多数时候, 异步通道有助于提升性能, 减少排队阻塞.
- 内置函数 cap和len返回缓冲区大小少当前已缓冲数量; 而对于同步通道则都返回0, 据此可判断通道是同步还是异步.
- 除使用简单的发送接收操作符外, 还可用ok-idom或range模式处理数据
package main
// 使用ok-idom模式处理
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done) // 确保发出结束通知
for {
x, ok := <-c
if !ok { // 据此判断通道是否关闭
return
}
println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done
}
package main
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for x := range c { // 循环获取消息, 直到通道被关闭
println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done
}
及时用close函数关闭通道引发结束通告, 否则可能会引起死锁
通告可以是群体性的,也未必就是通告结束, 可以是任何需要表达的事件
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ready := make(chan struct{})
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
println(id, ":ready.") // 运动员准备就绪
<-ready // 等待发令枪响
println(id, ": running...")
}(i)
}
time.Sleep(time.Second)
println("ready? GO!")
close(ready) // 发令枪: 砰!
wg.Wait()
}
一次性事件用close效率更好, 没有多余开销, 连续或多样性事件, 可传递不同数据标志实现. 还可使用
sync.Cond
实现单播或广播事件.
对于closed或nil通道, 发送和接收操作都有相应规则:
- 向已关闭的通道发送数据,会引发panic
- 从已关闭的通道接收数据, 返回已缓冲数据或零值
- 无论收发, nil通道都会阻塞
package main
func main() {
c := make(chan int, 3)
c <- 10
c <- 20
close(c)
for i := 0; i < cap(c)+1; i++ {
x, ok := <-c
println(i, ":", ok, x)
}
}
// 0 : true 10
// 1 : true 20
// 2 : false 0
// 3 : false 0
重复关闭,或关闭nil通道都会引发panic发错误 ,
单向通道
通道是默认双向的,并不区分发送和接收端,但某些时候,我们可以限制收发操作的方向, 来获得更严谨的操作逻辑
package main
import (
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan<- int = c
var recv <-chan int = c
go func() {
defer wg.Done()
for x := range recv {
println(x)
}
}()
go func() {
defer wg.Done()
defer close(c)
for i := 0; i < 3; i++ {
send <- i
}
}()
wg.Wait()
}
- 不能在单向通道上做逆向操作.
- close不能用于接收端
- 无法将单向通道重新转换回去.
通道选择
如果要同时处理多个通道 , 可选用select语句.它会随机选择一个可用通道做收发操作.
import (
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
a, b := make(chan int), make(chan int)
// 接收端
go func() {
defer wg.Done()
for {
var (
name string
x int
ok bool
)
select { // 随机选择可用channel接收数据
case x, ok = <-a:
name = "a"
case x, ok = <-b:
name = "b"
}
if !ok { // 如果任一通道关闭, 则终止接收
return
}
println(name, x) // 输出接收到数据
}
}()
// 发送端
go func() {
defer wg.Done()
defer close(a)
defer close(b)
for i := 0; i < 10; i++ {
select { // 随机选择发送端
case a <- i:
case b <- i * 10:
}
}
}()
wg.Wait()
}
- 当所有通道都不可用时, select会执行default语句. 如此可避开select阻塞, 但须注意处理外层循环, 以免陷入空耗.
- 当前通道已满, 可以使用default生成新的缓存通道
模式
通常使用工厂方法将 goroutine和通道绑定.
package main
import "sync"
type receiver struct {
sync.WaitGroup
data chan int
}
func newReceiver() *receiver {
r := &receiver{
data: make(chan int),
}
r.Add(1)
go func() {
defer r.Done()
for x := range r.data { //接收消息,直到通道关闭
println("recv:", x)
}
}()
return r
}
func main() {
r := newReceiver()
r.data <- 1
r.data <- 2
close(r.data) // 关闭通道, 发出结束通知
r.Wait() // 等待接收者处理结束
}
// recv: 1
// recv: 2
性能
- 将发往通道的数据打包, 减少传输次数, 可有效提升性能.单次获取更多数据, 可改善因频繁加锁造成的性能问题,
- 虽然单次消耗内存, 但性能提升非常明显.
8.3 同步
- 通道并非用来取代锁的, 它们有各自不同的使用场景, 通道倾向于解决逻辑层次的并发处理架构, 而锁则用来保护局部范围内的数据安全.
- 标准库sync 提供互斥和读写锁, 另有原子操作等, 可基本满足日常开发需要. Mutex, RWmutex的使用并不复杂, 只有几个地方需要注意.
- 将Mutex作为匿名字段时, 相关方法必须实现为pointer-receiver, 否则会因复制导致锁机制失效.
- 对性能要求较高时, 应避免使用defer Unlock
- 读写并发时, 用RWMutex性能会更好一些.
- 对单个数据读写保护, 可尝试用原子操作
- 执行严格测试, 尽可能打开数据竞争检查.