map 的一个神奇的问题

2021-04-06 11:54:56 +08:00
 zkdfbb

使用下面的一段代码来统计链接的访问情况,使用方法就是用 Incr 每次访问加 1 单独测试的时候,比如用 wrk 来压测都挺正常的 神奇的是,一换成 nginx,c.data 里面的 key 就变得不正常,把他打印出来,发现有很多相同的 key 比如本来就只有一个 key 的,他会出现 key: 1, key: 2, key: 1, key: 1 这种,搞不懂。。。

type Counter struct {
	sync.RWMutex
	data map[string]int
}

func (c *Counter) Get(key string) int {
	c.RLock()
	count := c.data[key]
	c.RUnlock()
	return count
}

func (c *Counter) Incr(key string) int {
	c.Lock()
	c.data[key]++
	count := c.data[key]
	c.Unlock()
	return count
}

func (c *Counter) Delete(key string) {
	c.Lock()
	delete(c.data, key)
	c.Unlock()
}
6617 次点击
所在节点    Go 编程语言
76 条回复
ZSeptember
2021-04-06 16:57:42 +08:00
可以把实际重复的 key 发出来看看?
问题应该还是 看似相同的 key 实际不同,但是具体为什么不同得具体分析了。
zkdfbb
2021-04-06 17:25:37 +08:00
@ZSeptember
@makdon
@Lpl

就这样,直接打印 accessLog.data 的话,有重复的 id
![]( https://p3-tt-ipv6.byteimg.com/origin/pgc-image/29af76c21c3f4e148252613ab6754a7c)
zkdfbb
2021-04-06 17:28:30 +08:00
然后如果打印 tmp 的话,因为 json 格式化之后,重复的 id 都去掉了,就会变的特别少
这个图,上面是打印 accessLog.data, 下面是打印 tmp

https://p26-tt.byteimg.com/origin/pgc-image/55a1b5f43bce43058915b2a74561f1d6
joesonw
2021-04-06 17:33:40 +08:00
枷锁了 print accessLog.data?
lesismal
2021-04-06 17:39:33 +08:00
accessLog = Counter{data: make(map[string]int)}
先改成
accessLog = &Counter{data: make(map[string]int)}
否则可能 panic

key 的问题,Counter 的 func 都把 key 先 trim 一下

@Lpl
另外,9 楼 3 条没一个是对的
lesismal
2021-04-06 17:41:04 +08:00
Lpl
2021-04-06 17:46:11 +08:00
@zkdfbb 没遇见过,看了 StackOverflow 上有个问题跟你这个有点像: https://stackoverflow.com/questions/41064208/go-map-has-duplicate-keys/41102560

看能否在 Incr 的时候,打印一下 key 的 string 串,再用 %x 打印一下 hex 做一下对比。
然后在输出 map 的时候,for-range 遍历下,也罢 key 的 string 和 hex 都打印下看看
Lpl
2021-04-06 17:51:37 +08:00
@lesismal 能不能详细展开说说
1. atomic 做的原子操作是比加锁快吧
2. 用管道通过求摸建立多个协程来消费
3. 目的是为了每一个 key 都能并发安全,加细粒度的锁不用去加对象锁,concurrentmap 不就是这样做的吗
lesismal
2021-04-06 18:23:12 +08:00
@Lpl

1. atomic 做的原子操作是比加锁快吧

先看 3 的回答吧,因为锁不可避免,所以在这里根本不能使用 atomic

并且因为是按 map 的 key 进行统计,首先你得取这个 map key 对应的 value,而直接取 map[key] 的地址是不行的,你试试编译下:
m := map[string]int32{"a": 0}
atomic.AddInt32(&m["a"], 1)
会报错: "cannot take the address of m["a"]"


2. 用管道通过求摸建立多个协程来消费

相对比较复杂的并且需要保证临界区一致性的并发逻辑可以考虑用 chan 替代锁来避免锁的复杂度尤其死锁等情况,但是简单功能,就比如这种计数器,使用 chan 实现起来会比锁更麻烦并且性能稍微损失一点点,完全没必要,要从实际出发、不能照本宣科

3. 目的是为了每一个 key 都能并发安全,加细粒度的锁不用去加对象锁,concurrentmap 不就是这样做的吗

首先要解决获得这个 key 的 value,获得这个 key 本身就需要对 map 的锁
标准库的 sync.Map 一样内置锁来实现、只是应用层不需要自己使用锁罢了
其他的三方 concurrentmap 实现也并不能实现每个 key 粒度,而是为了减少 key 数量巨大时并发流的竞争,所以在标准库 map 之上再加一层 hash buckets,再每个 buckets 对应的结构上用一个锁,go 标准库自己的 map 本身就是个 hash 下面多个 buckets,三方 concurrentmap 相当于再加一层 hash 分开成多个锁来降低粒度为 bucket 级别减少竞争
makdon
2021-04-06 18:23:40 +08:00
@Lpl
感觉你的 3 条怪怪的,但是说不出很具体很有说服力的理由。
尝试列举一下原因:
1. map 是用不到 atomic 的吧,没有 Google 到 atomic 跟 map 一起用的方法,如果 atomic.AddInt32(&m["a"],1) 会报错 can not take the address of m["id"],。
就算假设我们可以拿到 addr,那我们还需要额外处理 key 不存在的时候,map 桶分配等逻辑,这时候还需要两次 hash (查询是否存在时计算一次 hashkey,插入 /递增的时候再计算一次)。hash 应该比锁重多了吧。

2. 管道里面还是包含了一个锁,只是是拥有锁的时长变了,从“hash 需要的时间”变成了“写 channel 的时间”,但是 map 还是需要排队写入,引入个队列相当于脱裤子放屁,还引入了协程调度的开销。在这个 case 里面,map 应该是瓶颈,引入队列并没有解决这个问题。

3. 这种锁设计,根据官方文档“The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.” 很明显不属于前者,然后需要 ++;后者也不属于,因为 id 在生产上更可能是均匀的,不同协程写的值域是有交集的,仍然会产生锁竞争,性能不一定比 map with RWMutex 好
lesismal
2021-04-06 18:26:00 +08:00
@Lpl
我上一楼的回答的 1 中,即使 map 的 value 使用对象,然后 atomic 操作对象的 value 也不适合,因为最早 map 为空,拿不到对应的 value,如果判断空先存入,那还是要加锁,除非你的 key 数量和 key 值都是固定的、创建 map 时就初始化了 value
makdon
2021-04-06 18:26:36 +08:00
@lesismal
几乎同时回复了同一楼,不过大佬你讲得好多了😂
GTim
2021-04-06 18:32:00 +08:00
不是锁出了问题,而是 `key` 出了问题,可能存在空格或者不可见字符等
Orlion
2021-04-06 18:34:07 +08:00
init 函数中 accessLog = Counter{data: make(map[string]int)}这句代码不是原子的吧,换成
accessLog.Lock()
accessLog.data = make(map[string]int)
accessLog.RUnlock()

试试呢?
love2020
2021-04-06 18:53:13 +08:00
一个 map 我 瞬间进入知识盲区了
lesismal
2021-04-06 19:03:45 +08:00
@makdon 过奖了,3q
lesismal
2021-04-06 19:07:14 +08:00
@Orlion 对,这份代码里问题最大的就这句,accessLog 原来的内存被赋为新值的过程不原子,所以可能 panic 、还可能 Incr 的 Lock 是原来的 Mux 、Unlock 是新的 Mux 所以死锁

我还是建议我 25 楼说的那样都改成 accessLog = &Counter{data: make(map[string]int)}
sxfscool
2021-04-06 19:19:55 +08:00
```
func main() {
m := map[string]int{
"abcd": 0,
"abcd\x00": 1,
}
fmt.Println(m)
}
```
会打印 map[abcd:0 abcd:1]
试下 trim 掉空白字符试试
sxfscool
2021-04-06 19:28:30 +08:00
newString := strings.Map(func(r rune) rune {
if unicode.IsGraphic(r) {
return r
}
return -1
}, yourString)
用这个处理你的输入字符串后再放到 map 里
kcojkcnw
2021-04-06 19:35:24 +08:00
@lesismal 取地址这里可以理解为一个 64 位机子的 machine word 为 8byte (即 CPU 能保证的原子操作大小),刚好是一个 pointer 的大小,所以这句是原子的,是嘛?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/768320

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX