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

14 回复

谢谢

更多关于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
}

主要修改点:

  1. 重构了Entries类型:现在它是一个结构体,包含一个互斥锁和实际的映射字段
  2. 使用结构体级别的锁:在Adder方法中,使用Entries的互斥锁来保护对整个映射的所有访问操作
  3. 简化了锁的使用:移除了quantity结构体中的锁,因为现在所有操作都在Entries的锁保护下执行
  4. 使用defer确保解锁:这样即使发生panic也能保证锁被释放

这个版本确保了:

  • 映射的读取和写入操作是原子的
  • 不会出现并发映射写入错误
  • 所有对映射和其值的访问都是线程安全的

运行结果应该是稳定的,每次都会输出:100.00: 6.00(因为有6个goroutine各增加了1)

回到顶部