Golang中使用RWLock解决并发map写入问题
Golang中使用RWLock解决并发map写入问题 我正在尝试创建一个能够存储价格和数量的映射,希望映射值能够线程安全。我使用了sync.RWLock,但如果反复运行,第6或第8次就会出现并发映射写入错误。
[代码]: https://play.golang.org/p/PuRiyrvqoQI
package main
import (
"fmt"
"time"
"sync"
)
type quantity struct {
sync.RWMutex
amount float64
}
type Entries map[float64]*quantity
func main() {
vals:=Entries{}
go vals.Adder(100,1)
go vals.Adder(100,1)
go vals.Adder(100,1)
go vals.Adder(100,1)
go vals.Adder(100,1)
go vals.Adder(100,1)
time.Sleep(time.Millisecond*1)
for i,val:= range vals{
fmt.Println(i,": ",val)
}
}
func (m Entries)Adder(price,amnt float64) {
_,ok:=m[price]
if !ok {
m[price]=&quantity{}
m[price].Lock()
m[price].amount+=amnt
m[price].Unlock()
return
}
m[price].Lock()
m[price].amount+=amnt
m[price].Unlock()
}
更多关于Golang中使用RWLock解决并发map写入问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html
在真实负载和规模下进行适当的基准测试。
但这种做法会阻塞其他goroutine执行操作,可能导致较长的延迟
那么关于 sync.map,它是锁定整个映射还是仅锁定键的条目?
那么我应该使用 sync.Map 还是常规的 map?我已经用 map 开发了大量功能,但并发写入引发的 panic 是个大问题。
我理解您的观点"在所有操作都加锁,直到这真正成为性能瓶颈",感谢您提醒不要在涉及真实货币时使用浮点数。
从我快速浏览源码来看,它似乎对映射的缓存副本和一个脏标志进行了些处理,但我不确定这是否比简单锁定所有内容性能更好。另外请注意,它在锁定时使用的是常规的互斥锁。
你使用锁的方式很奇怪。我建议你使用一个全局的互斥锁变量:
var mu = &sync.Mutex{}
然后根据需要锁定/解锁对映射的访问:
mu.Lock()
...map operation (read/write)
mu.Unlock()
如果涉及真实用户的真实货币,请首先摒弃浮点数类型,谷歌会告诉你为什么不应使用浮点数处理货币金额。
然后采用最直接的方法,将所有操作加锁处理,直到这真正成为性能瓶颈为止。切勿在生产代码中试图优化那些尚未实际出现的问题。
《Go程序设计语言》一书中,第9.7章 并发非阻塞缓存 提供了关于如何对映射条目而非整个映射进行加锁的精彩示例 😉。请查阅 https://github.com/adonovan/gopl.io/tree/master/ch9
尽管你分别对值进行了加锁和解锁操作,但仍然存在对存储这些锁的映射表的并发访问。
你需要对整个映射表进行加锁。没有其他方法,这是唯一途径。
如果在实际写入时谨慎使用 Lock() 并在大部分时间使用 RLock(),情况就不会那么糟糕。
func main() {
fmt.Println("hello world")
}
taalhach:
但这种方法会阻塞其他 Go 协程执行操作,可能导致较长的延迟
在这种情况下,你可以使用信号量实现自己的动态结构。但这仅在你确实需要处理大型数据结构时才适用。如果数据量不大,请放心使用 map,因为它本身就是动态结构且运行效率很高。
[补充] 与其使用单个大型 map,可以尝试使用多个小型 map 并将其组织成 map 数组或其他结构,然后根据需要分别锁定各个小型 map。
目前我正在开发一个交易所项目,需要为订单提供内存存储以实现快速订单匹配(这些订单也会存储在数据库中)。当前我使用映射结构,以价格作为键,订单数组或切片作为对应键的值。
结构如下所示: 价格 : 订单 1.02 : [ ]订单 1.03 : [ ]订单
我需要从映射中读取数据,并根据新到达的订单修改订单数组。 问题在于映射不是线程安全的,因此会出现并发写入导致的异常。 如果使用锁机制,会锁定整个映射,导致毫秒级的延迟。对于这种数据结构,您有什么建议?
在你的代码中,并发写入错误的主要问题在于对映射本身的并发访问没有进行保护。虽然你为每个quantity结构体使用了读写锁来保护amount字段的并发访问,但映射Entries的读写操作(包括检查键是否存在、插入新键等)并没有被同步。
当多个goroutine同时执行Adder方法时,可能会同时检查某个价格键是否存在,然后都尝试创建新的quantity实例并插入映射,这就导致了并发映射写入。
以下是修复后的代码:
package main
import (
"fmt"
"sync"
"time"
)
type quantity struct {
amount float64
}
type Entries struct {
sync.RWMutex
entries map[float64]*quantity
}
func main() {
vals := &Entries{
entries: make(map[float64]*quantity),
}
for i := 0; i < 6; i++ {
go vals.Adder(100, 1)
}
time.Sleep(time.Millisecond * 10)
vals.RLock()
defer vals.RUnlock()
for price, q := range vals.entries {
fmt.Printf("%.2f: %.2f\n", price, q.amount)
}
}
func (m *Entries) Adder(price, amnt float64) {
m.Lock()
defer m.Unlock()
q, exists := m.entries[price]
if !exists {
q = &quantity{}
m.entries[price] = q
}
q.amount += amnt
}
主要修改点:
- 重构了
Entries类型:现在它是一个结构体,包含一个互斥锁和实际的映射字段 - 使用结构体级别的锁:在
Adder方法中,使用Entries的互斥锁来保护对整个映射的所有访问操作 - 简化了锁的使用:移除了
quantity结构体中的锁,因为现在所有操作都在Entries的锁保护下执行 - 使用
defer确保解锁:这样即使发生panic也能保证锁被释放
这个版本确保了:
- 映射的读取和写入操作是原子的
- 不会出现并发映射写入错误
- 所有对映射和其值的访问都是线程安全的
运行结果应该是稳定的,每次都会输出:100.00: 6.00(因为有6个goroutine各增加了1)

