假设我们需要处理很多任务;一个worker处理一项任务。任务可以被定义为一个结构体(具体的细节在这里并不重要):
~~~
type Task struct {
// some state
}
~~~
## 锁模式:使用共享内存进行同步
由各个任务组成的任务池共享内存;为了同步各个worker以及避免资源竞争,我们需要对任务池进行加锁保护:
~~~
type Pool struct {
Mu sync.Mutex
Tasks []*Task
}
~~~
sync.Mutex是互斥锁:它用来在代码中保护临界区资源:同一时间只有一个go协程(goroutine)可以进入该临界区。
如果出现了同一时间多个go协程都进入了该临界区,则会产生竞争:Pool结构就不能保证被正确更新。worker代码可能这样写:
~~~
func Worker(pool *Pool) {
for {
pool.Mu.Lock()
// begin critical section:
task := pool.Tasks[0] // take the first task
pool.Tasks = pool.Tasks[1:] // update the pool of tasks
// end critical section
pool.Mu.Unlock()
process(task)
}
}
~~~
这些worker有许多都可以并发执行;他们可以在go协程中启动。一个worker先将pool锁定,从pool获取第一项任务,再解锁和处理任务。加锁保证了同一时间只有一个go协程可以进入到pool中:一项任务有且只能被赋予一个worker。如果不加锁,则工作协程可能会在`task:=pool.Tasks[0]`发生切换,导致`pool.Tasks=pool.Tasks[1:]`结果异常:一些worker获取不到任务,而一些任务可能被多个worker得到。加锁实现同步的方式在工作协程比较少时可以工作的很好,但是当工作协程数量很大,任务量也很多时,处理效率将会因为频繁的加锁/解锁开销而降低。当工作协程数增加到一个阈值时,程序效率会急剧下降,这就成为了瓶颈。
## 通道模式:使用通道
使用通道进行同步:
使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。
worker在协程中启动,其数量N应该根据任务数量进行调整。
主线程扮演着Master节点角色,可能写成如下形式:
~~~
func main() {
pending, done := make(chan *Task), make(chan *Task)
go sendWork(pending) // put tasks with work on the channel
for i := 0; i < N; i++ { // start N goroutines to do work
go Worker(pending, done)
}
consumeWork(done) // continue with the processed tasks
}
~~~
worker的逻辑比较简单:从pending通道拿任务,处理后将其放到done通道中:
~~~
func Worker(in, out chan *Task) {
for {
t := <-in
process(t)
out <- t
}
}
~~~
这里并不使用锁:从通道得到新任务的过程没有任何竞争。随着任务数量增加,worker数量也应该相应增加,同时性能并不会像第一种方式那样下降明显。在pending通道中存在一份任务的拷贝,第一个worker从pending通道中获得第一个任务并进行处理,这里并不存在竞争(对一个通道读数据和写数据的整个过程是原子性的)。某一个任务会在哪一个worker中被执行是不可知的,反过来也是。worker数量的增多也会增加通信的开销,这会对性能有轻微的影响。
从这个简单的例子中可能很难看出第二种模式的优势,但含有复杂锁运用的程序不仅在编写上显得困难,也不容易编写正确,使用第二种模式的话,就无需考虑这么复杂的东西了。
因此,第二种模式对比第一种模式而言,不仅性能是一个主要优势,而且还有个更大的优势:代码显得更清晰、更优雅。一个更符合go语言习惯的worker写法:
**IDIOM: Use an in- and out-channel instead of locking**
~~~
func Worker(in, out chan *Task) {
for {
t := <-in
process(t)
out <- t
}
}
~~~
对于任何可以建模为Master-Worker范例的问题,一个类似于worker使用通道进行通信和交互、Master进行整体协调的方案都能完美解决。如果系统部署在多台机器上,各个机器上执行Worker协程,Master和Worker之间使用netchan或者RPC进行通信(参见15章)。
怎么选择是该使用锁还是通道?
通道是一个较新的概念,本节我们着重强调了在go协程里通道的使用,但这并不意味着经典的锁方法就不能使用。go语言让你可以根据实际问题进行选择:创建一个优雅、简单、可读性强、在大多数场景性能表现都能很好的方案。如果你的问题适合使用锁,也不要忌讳使用它。go语言注重实用,什么方式最能解决你的问题就用什么方式,而不是强迫你使用一种编码风格。下面列出一个普遍的经验法则:
* 使用锁的情景:
* 访问共享数据结构中的缓存信息
* 保存应用程序上下文和状态信息数据
* 使用通道的情景:
* 与异步操作的结果进行交互
* 分发任务
* 传递数据所有权
当你发现你的锁使用规则变得很复杂时,可以反省使用通道会不会使问题变得简单些。
- go入门
- go基础
- go语言介绍
- go语言主要特性
- Golang内置类型和函数
- init函数和main函数
- 下划线
- iota
- 字符串
- 数据类型:数组与切片
- 数据类型:byte、rune与字符串
- 变量的5种创建方式
- 数据类型:字典
- 指针
- 数据类型:指针
- 类型断言
- 流程控制:defer延迟执行
- defer陷进
- 异常机制:panic和recover
- go函数
- go方法
- go依赖管理
- 轻松搞懂goroot与gopath区别
- 使用go module导入本地包的方法教程详解
- 读取用户的输入
- 文件读写
- 文件拷贝
- 从命令行读取参数
- JSON 数据格式
- 4 种常见JSON 格式数据解码
- XML 数据格式
- 用 Gob 传输数据
- Go 中的密码学
- 学习资料建议
- 深入结构体
- 测试
- 单元测试
- 常用标准库
- fmt
- time
- flag
- log
- IO操作
- 文件读取
- strconv
- template
- http
- context
- json
- 从文件中反序列化json对象
- xml
- go proxy 设置
- 面向对象
- 结构体
- struct能不能比较
- 接口
- make和new的区别
- go进阶
- Slice底层实现
- 闭包与递归
- 空接口
- 反射
- 接口中的“坑”
- 反射三定律
- 结构体里的tag
- 并发编程
- 初识Go 协程:goroutine
- go协程:管道
- 任务和master-锁实现和通道实现
- 惰性生成器的实现
- runtime包
- Goroutine池
- 定时器
- 并发安全和锁
- Sync
- 原子操作(atomic包)
- GMP 原理与调度
- 爬虫案例
- 邮件发送
- Godoc 安装与使用
- test
- 如何测试
- 基准测试
- 数组与切片
- 结构体,方法和接口
- Map实现原理
- 自定义error
- 网络编程
- socket编程
- 互联网协议
- tcp 服务器
- tcp编程
- UDP编程
- TCP黏包
- http编程
- websocket编程
- 设计模式
- 设置模式6大原则
- 创建型模式
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
- 创建者模式
- 原型模式
- 单例模式
- 结构性模式
- 外观模式
- 适配器模式
- 代理模式
- 组合模式
- 享元模式
- 装饰模式
- 桥模式
- 行为型模式
- 中介者模式
- 观察者模式
- 命令模式
- 迭代器模式
- 模板方法模式
- 策略模式
- 状态模式
- 备忘录模式
- 解释器模式
- 职责链模式
- 访问者
- rpc
- Golang内存分配逃逸分析
- 面试题汇总
- 信号量的原理与使用
- 如何让在强制转换类型时不发生内存拷贝
- Go 如何利用 Linux 内核的负载均衡能力
- 性能优化:Go Ballast 让内存控制更加丝滑
- unsafe包详解
- go实战
- Go语言中编码规范
- json如何转为struct对象
- cobra
- 通过go mod模式创建cobra项目
- gorm
- gocache
- zap日志库
- echart
- web技术
- niugo
- context回调实现原理
- 认证与授权
- oauth2.0的4种实现方式
- IRIS
- 安装
- 入门
- 自定义http错误
- 基本HTTP API
- 中间件
- session
- websocket
- mvc
- cookie使用
- Casbin
- CORS跨域资源共享
- csrf防御
- jwt
- 限制HTTP请求次数的中间件Tollbooth
- 文件服务
- 基础使用
- 文件下载
- hero依赖注入与结构体转化
- hero基础
- 网络教程
- gin
- viper
- 在 5 分钟之内部署一个 Go 应用(Supervisor )
- go如何正常go get导入包
- 杂项
- 开源许可证
- 算法
- 洗牌算法
- 经典算法
- 基排序
- 冒泡排序
- 选择排序算法
- 二叉树
- 堆排序
- 快速排序
- 二分查找
- 图算法
- 有向图结构实现
- 拓扑排序
- 一致性hash算法
- 前缀树(字典树)
- 算法实现
- 斐波拉契
- 加密算法
- 简单可逆加密
- DH密钥交换(Diffie–Hellman key exchange)算法
- 代码实现
- Polybius密码(棋盘密码
- xor加密算法
- go应用
- 调试
- 构建并运行
- 包别名
- 类型转换
- error错误的2种处理方式
- 使用defer实现代码追踪
- 计算函数执行时间
- 通过内存缓存来提升性能
- make和new
- 关闭的channel可以读取数据吗
- 如何优雅的关闭channel
- channel应用场景
- map相关问题
- Go 面向包的设计和架构分层
- 设计模式实战
- 模板模式
- 责任链模式
- 组合模式实战
- 观察者模式实战
- 状态模式实战
- 区块链
- 构建一个区块链 -- Part 1: 基本原型
- 构建一个区块链 -- Part 2: 工作量证明
- 构建一个区块链 -- Part 3:持久化和命令行接口
- 从0到精通
- go常用命令
- 获取命令行参数
- http服务
- 基础
- struct 5种实例化
- md5
- Go Protobuf入门