[go语言]吐槽:怎么样实现支持并发访问的数据集合更好?
时间:2022-05-05
本文章向大家介绍[go语言]吐槽:怎么样实现支持并发访问的数据集合更好?,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
在go语言里,提倡用信道通讯的方式来替代显式的同步机制。但是我发现有的时候用信道通讯方式实现的似乎也不是很好(暂不考虑效率问题)。
假设有一个帐号的集合,需要在这个集合上实现一些操作,比如查找修改等。这个集合的操作必须是支持并发的。
如果用锁的方式(方案1)实现大概是这样:
import "sync"
type Info struct {
age int
}
type AccountMap struct {
accounts map[string]*Info
mutex sync.Mutex
}
func NewAccountMap() *AccountMap {
return &AccountMap{
accounts: make(map[string]*Info),
}
}
func (p *AccountMap) add(name string, age int) {
p.mutex.Lock()
defer p.mutex.Unlock()
p.accounts[name] = &Info{age}
}
func (p *AccountMap) del(name string) {
p.mutex.Lock()
defer p.mutex.Unlock()
delete(p.accounts, name)
}
func (p *AccountMap) find(name string) *Info {
p.mutex.Lock()
defer p.mutex.Unlock()
res, ok := p.accounts[name]
if !ok {
return nil
}
inf := *res
return &inf
}
用信道来实现试试(方案2):
type Info struct {
age int
}
type AccountMap struct {
accounts map[string]*Info
ch chan func()
}
func NewAccountMap() *AccountMap {
p := &AccountMap{
accounts: make(map[string]*Info),
ch: make(chan func()),
}
go func() {
for {(<-p.ch)()}
}()
return p
}
func (p *AccountMap) add(name string, age int) {
p.ch <- func() {
p.accounts[name] = &Info{age}
}
}
func (p *AccountMap) del(name string) {
p.ch <- func() {
delete(p.accounts, name)
}
}
func (p *AccountMap) find(name string) *Info {
// 每次查询都要创建一个信道
c := make(chan *Info)
p.ch <- func() {
res, ok := p.accounts[name]
if !ok {
c <- nil
} else {
inf := *res
c <- &inf
}
}
return <-c
}
这里有个问题,每次调用find都要创建一个信道。
那么试试把信道作为参数(方案3),只需要修改find函数的实现:
// 信道对象作为参数,暴露了实现机制
func (p *AccountMap) find(name string, c chan *Info) *Info {
p.ch <- func() {
res, ok := p.accounts[name]
if !ok {
c <- nil
} else {
inf := *res
c <- &inf
}
}
return <-c
}
总结一下,现在的问题就是三种方案都有不尽如人意之处:
方案1:使用锁机制,不太符合go解决问题的方式。
方案2:对于需要返回结果的查询,每次查询都要创建一个信道,比较浪费资源。
方案3:需要在函数参数中指定信道对象,把实现机制暴露了。
那么有没有什么更好的方案呢?
2012.12.14:
方案2还有一个改进版本:利用预分配以及可回收的channel来提高资源利用率。这个技术在多个goroutine等待一个主动对象返回自己的数据时会比较有用。例如网游服务器中登录服务器里每个玩家的连接用一个goroutine来处理;另外一个主动对象代表帐号服务器连接用于验证帐号合法性。玩家goroutine会把各自的输入的玩家帐号密码发送给这个主动对象,并阻塞等待主动对象返回验证结果。因为有多个玩家同时发起帐号验证请求,所以主动对象需要把返回结果进行分发,因此可以在发送请求的时候申请一个信道并等待这个信道。
代码如下:
type Info struct {
age int
}
type AccountMap struct {
accounts map[string]*Info
ch chan func()
tokens chan chan *Info
}
func NewAccountMap() *AccountMap {
p := &AccountMap{
accounts: make(map[string]*Info),
ch: make(chan func()),
tokens: make(chan chan *Info, 128),
}
for i := 0; i < cap(p.tokens); i++ {
p.tokens <- make(chan *Info)
}
go func() {
for {(<-p.ch)()}
}()
return p
}
func (p *AccountMap) add(name string, age int) {
p.ch <- func() {
p.accounts[name] = &Info{age}
}
}
func (p *AccountMap) del(name string) {
p.ch <- func() {
delete(p.accounts, name)
}
}
func (p *AccountMap) find(name string) *Info {
// 每次查询都要获取一个信道
c := <-p.tokens
p.ch <- func() {
res, ok := p.accounts[name]
if !ok {
c <- nil
} else {
inf := *res
c <- &inf
}
}
inf := <-c
// 回收信道
p.tokens <- c
return inf
}
补充一下golang-china上的评论:
xushiwei
在你的方式里面,用 channel 其实把所有请求串行化。
另外,从成本上来说,channel 远大于锁。因为 channel 本身显然是用锁 + 信号唤醒机制实现的。
steve wang
是不是可以这样总结:
1.对于共享给各个goroutine的数据对象的并发访问,使用锁来控制
2.对于goroutine之间的通信,使用信道
longshanksmo
单就性能来看,现在下这种结论有些草率。并发和性能问题错宗复杂,不同的场景可能会产生完全相反的结论。
还有众多因素需要考虑:
首先,不同的用况下,锁粒度不同。在你的案例中是map操作,锁粒度很小。但如果是某种重载操作,或者存在阻塞,锁粒度会很大。那时用锁就不划算。
其次,chan的锁粒度很小,基本固定,可预测。在实际业务中,性能可预测非常重要,决定了部署时的资源投入和调配。
最重要一点,如果进程内的所有goroutine是在单个线程内运行,那么chan的锁是不需要的。这样才能真正发挥coroutine的优势。现在的go编译器似乎还没有对这个做优化,不知将来是否会进化。
总之,并发方面还没有一改而论
- 洛谷P3038 [USACO11DEC]牧草种植Grass Planting
- 【 关关的刷题日记47】Leetcode 38. Count and Say
- 《Python自然语言处理》答案第一、二章
- 【 关关的刷题日记49】 Leetcode 434. Number of Segments in a String
- 自然语言处理构建文本向量空间1.百科2.源代码3.参考:
- 小爬虫之爬取豆瓣电影排行榜1.技术路线2.任务3.分析4.运行结果5.源码
- Numpy 修炼之道 (5)—— 索引和切片
- 深入理解final关键字
- Numpy 修炼之道 (4)—— 基本运算操作
- 一些APT攻击案例分享
- 浅谈命令查询职责分离(CQRS)模式
- Numpy 修炼之道 (3)—— 数据类型
- 熔断器设计模式
- 树链剖分详解
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- C# ref实例讲解
- [Oracle 故障处理]记一次undo表空间使用率99%的问题
- C# 队列(Queue)
- 记住没:永远不要在 MySQL 中使用 UTF-8!
- C#多线程委托ParameterizedThreadStart应用
- [Python爬虫]使用Selenium操作浏览器订购火车票
- IDEA多线程调试
- 为什么wait和notify方法要在同步块中调用?
- 一文说清楚Mysql Innodb的B+树索引原理及其推理过程
- [Oracle数据泵全解析]expdp交互式命令行模式命令
- SpringBoot Feign文件上传
- Docker_000
- 如何应对面试官的JVM调优问题
- Docker_001
- Docker_002