Channel
CSP(Communicating Sequential Process):CSP 允许使用进程组件来描述系统,它们独立运行,并且只通过消息传递的方式通信
Channel 类型是 Go 语言内置的类型,可以直接使用
Don’t communicate by sharing memory, share memory by communicating. --Go Proverbs by Rob Pike
Channel用法:
- 数据交流:当作并发的 buffer 或者 queue,解决生产者 - 消费者问题。多个 goroutine 可以并发当作生产者(Producer)和消费者(Consumer)。
- 数据传递:一个 goroutine 将数据交给另一个 goroutine,相当于把数据的拥有权 (引用) 托付出去。
- 信号通知:一个 goroutine 可以将信号 (closing、closed、data ready 等) 传递给另一个或者另一组 goroutine 。
- 任务编排:可以让一组 goroutine 按照一定的顺序并发或者串行的执行,这就是编排的功能。
- 锁:利用 Channel 也可以实现互斥锁的机制。
chan string // 可以发送接收string
chan<- struct{} // 只能发送struct{}
<-chan int // 只能从chan接收int
ch <- 2000 // 发送数据
x := <-ch // 把接收的一条数据赋值给变量x foo(<-ch) // 把接收的一个的数据作为参数传给函数 <-ch // 丢弃接收的一条数据
close:关闭 chan 关闭掉
cap:返回 chan 的容量
chan 还可以应用于 for-range 语句中:
for v := range ch {
fmt.Println(v)
}
使用 Channel 容易犯的错误
panic
- close 为 nil 的 chan;
- send 已经 close 的 chan;
- close 已经 close 的 chan。
goroutine 泄漏
unbuffered chan(初始化时不指定容量)Writer和reader,这两个事件必须同时发生。如果一个先发生,它所在的 goroutine 就会被 Go 的调度器阻塞
并发原语和Channel的选择方法
- 共享资源的并发访问使用传统并发原语;
- 复杂的任务编排和消息传递使用 Channel;
- 消息通知机制使用 Channel,除非只想 signal 一个 goroutine,才使用 Cond;
- 简单等待所有任务的完成用 WaitGroup,也有 Channel 的推崇者用 Channel,都可以;
- 需要和 Select 语句结合,使用 Channel;
- 需要和超时配合时,使用 Channel 和 Context。
使用反射操作 Channel
通过 reflect.Select 函数,可以将一组运行时的 case clause 传入,当作参数执行,无需写100个Case,可以动态创建case
func main() {
var ch1 = make(chan int, 10)
var ch2 = make(chan int, 10)
// 创建SelectCase
var cases = createCases(ch1, ch2)
// 执行10次select
for i := 0; i < 10; i++ {
chosen, recv, ok := reflect.Select(cases)
if recv.IsValid() { // recv case
fmt.Println("recv:", cases[chosen].Dir, recv, ok)
} else { // send case
fmt.Println("send:", cases[chosen].Dir, ok)
}
}
}
func createCases(chs ...chan int) []reflect.SelectCase {
var cases []reflect.SelectCase
// 创建recv case
for _, ch := range chs {
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(ch),
})
}
// 创建send case
for i, ch := range chs {
v := reflect.ValueOf(i)
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectSend,
Chan: reflect.ValueOf(ch),
Send: v,
})
}
return cases
}
典型的应用场景
消息交流
chan的内部实现是循环队列,所以有时会被当成线程安全的队列和 buffer 使用
例子:
- 通过chan Job实现的worker 池
- etcd 中的 node节点
数据传递
令牌(token)传递
type Token struct{}
func newWorker(id int, ch chan Token, nextCh chan Token) {
for {
token := <-ch // 取得令牌
fmt.Println((id + 1)) // id从1开始
time.Sleep(time.Second)
nextCh <- token
}
}
func main() {
chs := []chan Token{make(chan Token), make(chan Token), make(chan Token), make(chan Token)}
// 创建4个worker
for i := 0; i < 4; i++ {
go newWorker(i, chs[i], chs[(i+1)%4])
}
//首先把令牌交给第一个worker
chs[0] <- struct{}{}
select {}
}
信号通知
使用 chan 实现程序的 graceful shutdown,在退出之前执行一些连接关闭、文件 close、缓存落盘等一些动作
func main() {
var closing = make(chan struct{})
var closed = make(chan struct{})
go func() {
// 模拟业务处理
for {
select {
case <-closing:
return
default:
// ....... 业务计算
time.Sleep(100 * time.Millisecond)
}
}
}()
// 处理CTRL+C等中断信号
termChan := make(chan os.Signal)
signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM)
<-termChan
close(closing)
// 执行退出之前的清理动作
go doCleanup(closed)
select {
case <-closed:
case <-time.After(time.Second):
fmt.Println("清理超时,不等了")
}
fmt.Println("优雅退出")
}
func doCleanup(closed chan struct{}) {
time.Sleep((time.Minute))
close(closed)
}
锁
互斥锁,利用 select+chan 的方式,很容易实现 TryLock、Timeout 的功能
任务编排
Or-Done 模式、扇入模式、扇出模式、Stream 和 map-reduce
Or-Done 模式
如果有多个任务,只要有任意一个任务执行完,我们就想获得这个信号
扇入模式
多个源 Channel 输入、一个目的 Channel 输出
扇出模式
扇出模式只有一个输入源 Channel,有多个目标 Channel
Stream
流式管道,提供跳过几个元素,或者是只取其中的几个元素等方法
map-reduce
第一步是映射(map),处理队列中的数据,第二步是规约(reduce),把列表中的每一个元素按照一定的处理方式处理成结果,放入到结果队列中
内存模型
多线程同时访问同一个变量的可见性和顺序
happens-before
👍在一个 goroutine 内部,程序的执行顺序和它们的代码指定的顺序是一样的,即使编译器或者 CPU 重排了读写顺序,从行为上来看,也和代码指定的顺序一样
😡但Go 只保证 goroutine 内部重排对读写的顺序没有影响,如果要保证多个 goroutine 之间对一个共享变量的读写顺序,在 Go 语言中,可以使用并发原语为读写操作建立 happens-before 关系
- 在 Go 语言中,对变量进行零值的初始化就是一个写操作。
- 如果对超过机器 word(64bit、32bit 或者其它)大小的值进行读写,那么,就可以看作是对拆成 word 大小的几个读写无序进行。
- Go 并不提供直接的 CPU 屏障(CPU fence)来提示编译器或者 CPU 保证顺序性,而是使用不同架构的内存屏障指令来实现统一的并发原语。
Go 语言中保证的 happens-before 关系
init 函数
main 函数一定在导入的包的 init 函数之后执行
goroutine
启动 goroutine 的 go 语句的执行,一定 happens before 此 goroutine 内的代码执行
Channel
- 第 1 条规则是,往 Channel 中的发送操作,happens before 从该 Channel 接收相应数据的动作完成之前,即第 n 个 send 一定 happens before 第 n 个 receive 的完成。
- 第 2 条规则是,close 一个 Channel 的调用,肯定 happens before 从关闭的 Channel 中读取出一个零值。
- 第 3 条规则是,对于 unbuffered 的 Channel,也就是容量是 0 的 Channel,从此 Channel 中读取数据的调用一定 happens before 往此 Channel 发送数据的调用完成。
- 第 4 条规则是,如果 Channel 的容量是 m(m>0),那么,第 n 个 receive 一定 happens before 第 n+m 个 send 的完成。
Mutex/RWMutex
- 第 n 次的 m.Unlock 一定 happens before 第 n+1 m.Lock 方法的返回;
- 对于读写锁 RWMutex m,如果它的第 n 个 m.Lock 方法的调用已返回,那么它的第 n 个 m.Unlock 的方法调用一定 happens before 任何一个 m.RLock 方法调用的返回,只要这些 m.RLock 方法调用 happens after 第 n 次 m.Lock 的调用的返回。这就可以保证,只有释放了持有的写锁,那些等待的读请求才能请求到读锁。
- 对于读写锁 RWMutex m,如果它的第 n 个 m.RLock 方法的调用已返回,那么它的第 k (k<=n)个成功的 m.RUnlock 方法的返回一定 happens before 任意的 m.RUnlockLock 方法调用,只要这些 m.Lock 方法调用 happens after 第 n 次 m.RLock。
WaitGroup
Wait 方法等到计数值归零之后才返回
Once
对于 once.Do(f) 调用,f 函数的那个单次调用一定 happens before 任何 once.Do(f) 调用的返回