英文源地址
在前面的例子中, 我们使用了带有互斥锁的显式锁来同步跨多个协程共享状态的访问.另一种选择是使用协程和通道的内置同步特性来实现相同的效果.这种基于通道的方法符合Go语言的思想,即通过通信共享内存, 并使每个数据块由一个协程拥有.
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
// 在这个例子中, 我们的状态由一个单独的协程拥有
// 这将保证数据永远不会因并发访问而损坏.
// 为了读取或写入状态, 其他协程将向所属的协程发送消息并接收相应的回复.
// 这些readOp和writeOp结构封装了这些请求, 并为所属的程序提供了一种响应方式.
type readOp struct {
key int
resp chan int
}
type writeOp struct {
key int
val int
resp chan bool
}
func main() {
// 和之前一样, 我们将计算执行了多少次操作
var readOps uint64
var writeOps uint64
// 其他协程分别使用读和写通道来发出读和写请求
reads := make(chan readOp)
writes := make(chan writeOp)
// 下面是拥有状态的运行协程, 它和前面的例子一样是一个映射.
// 但现在是有状态运行协程私有的.
// 该协程反复select读和写通道, 在请求到达时进行响应.
// 响应的执行方式是, 首先执行请求的操作, 然后在响应通道resp上发送一个值来表示成功(以及读取的情况下的期望值)
go func() {
var state = make(map[int]int)
for {
select {
case read := <-reads:
read.resp <- state[read.key]
case write := <-writes:
state[write.key] = write.val
write.resp <- true
}
}
}()
// 这将启动100个协程, 通过读通道向拥有state的协程发出读操作.
// 每次读取都需要构造一个readOp,通过读取通道发送, 然后通过提供的响应通道接受结果.
for r := 0; r < 100; r++ {
go func() {
for {
read := readOp{
key: rand.Intn(5),
resp: make(chan int),
}
reads <- read
<-read.resp
atomic.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
// 我们也开始10次写入, 使用类似的方法
for w := 0; w < 10; w++ {
go func() {
for {
write := writeOp{
key: rand.Intn(5),
val: rand.Intn(100),
resp: make(chan bool),
}
writes <- write
<-write.resp
atomic.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
// 让协程运行一秒钟
time.Sleep(time.Second)
// 最终, 捕获并打印操作次数
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps", writeOpsFinal)
}
运行我们的程序显示, 基于运行协程的状态管理示例总共完成了大约80000次操作.
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177
对于这种特殊情况, 基于协程的方法比基于互斥锁的方法更复杂一点. 但是, 在某些情况下它可能是有用的, 例如当涉及到其他通道时, 或者当管理多个这样的互斥锁容易出错时.你应该使用感觉最自然的方法, 特别是在理解程序的正确性方面.
下一节将介绍: 排序