← 中文资源

简体中文版本

分片并发映射

用一组独立加锁的分片降低锁竞争,让大量协程可以同时读写同一个映射而不互相阻塞。

阅读英文原文

功能说明

内置 map 不是并发安全的,包一层全局互斥锁虽然正确,但所有读写都会排队。分片映射把键空间按哈希分散到若干片,每片有独立的读写锁,不同片的操作可以真正并行。分片数量通常是二的幂或 CPU 核数的若干倍,示例使用八片。

这个结构适合进程内缓存、连接注册表、计数聚合等写多读也多的场景。每个分片内部仍然是普通 map,逻辑简单,便于审查。

关键代码

shardFor 用 FNV-1a 对键的字符串形式求哈希,再对分片数取模。Set 与 Delete 拿写锁,Get 拿读锁,Len 逐个分片读取长度并累加。main 启动十六个协程各写入一百条记录,用 WaitGroup 等待全部完成,然后验证总数、查询和删除。闭包把 worker 作为参数传入,避免循环变量捕获问题。

使用注意

Len 不是原子快照,遍历期间其他协程仍可能写入,得到的只是近似值。键的字符串化使用 fmt.Fprintf,存在分配开销,热点路径可以针对字符串键改用直接哈希。分片数量在创建后固定,不能动态扩容。没有提供范围遍历,需要遍历时应实现按片加读锁的 Range 回调。删除不存在的键不会报错。哈希碰撞只会让不同键落进同一片,不影响正确性,只影响并发度。

验证方式

十六个协程各写一百个不同键,最终 entries 应为 1600。查询 key-3-42 返回 42 和 true,删除后再次查询返回 false。可以在启动时加上竞态检测器运行,确认没有数据竞争报错;把分片数改为一,吞吐会明显下降,但结果保持一致。

代码示例

package main

import (
	"fmt"
	"hash/fnv"
	"sync"
)

type shard[K comparable, V any] struct {
	mu   sync.RWMutex
	data map[K]V
}

type ShardedMap[K comparable, V any] struct {
	shards []*shard[K, V]
}

func NewShardedMap[K comparable, V any](count int) *ShardedMap[K, V] {
	if count <= 0 {
		panic("shard count must be positive")
	}
	m := &ShardedMap[K, V]{shards: make([]*shard[K, V], count)}
	for i := range m.shards {
		m.shards[i] = &shard[K, V]{data: make(map[K]V)}
	}
	return m
}

func (m *ShardedMap[K, V]) shardFor(key K) *shard[K, V] {
	hasher := fnv.New32a()
	fmt.Fprintf(hasher, "%v", key)
	return m.shards[int(hasher.Sum32())%len(m.shards)]
}

func (m *ShardedMap[K, V]) Set(key K, value V) {
	s := m.shardFor(key)
	s.mu.Lock()
	s.data[key] = value
	s.mu.Unlock()
}

func (m *ShardedMap[K, V]) Get(key K) (V, bool) {
	s := m.shardFor(key)
	s.mu.RLock()
	value, ok := s.data[key]
	s.mu.RUnlock()
	return value, ok
}

func (m *ShardedMap[K, V]) Delete(key K) {
	s := m.shardFor(key)
	s.mu.Lock()
	delete(s.data, key)
	s.mu.Unlock()
}

func (m *ShardedMap[K, V]) Len() int {
	total := 0
	for _, s := range m.shards {
		s.mu.RLock()
		total += len(s.data)
		s.mu.RUnlock()
	}
	return total
}

func main() {
	cache := NewShardedMap[string, int](8)
	var wg sync.WaitGroup
	for worker := 0; worker < 16; worker++ {
		wg.Add(1)
		go func(worker int) {
			defer wg.Done()
			for i := 0; i < 100; i++ {
				cache.Set(fmt.Sprintf("key-%d-%d", worker, i), i)
			}
		}(worker)
	}
	wg.Wait()

	fmt.Println("entries:", cache.Len())
	value, ok := cache.Get("key-3-42")
	fmt.Println("lookup:", value, ok)
	cache.Delete("key-3-42")
	_, ok = cache.Get("key-3-42")
	fmt.Println("after delete:", ok)
}