概念概述
顺序锁(Seqlock)(Seqlocks)指的是:顺序锁让读者在检测到并发写入时重试,为极少写入而频繁读取的数据提供快速的无锁读取,但读者可能饥饿。
从更严格的定义看,A sequence lock is a synchronisation primitive that pairs a counter with shared data, letting readers detect a concurrent write and retry rather than take a lock. 该概念在工程实践中的价值,在于它能帮助团队用统一的语言描述一个具体的质量属性或设计约束,从而在评审与协作中减少歧义。
为什么需要它
It suits structures that are updated infrequently but read from hot paths, such as time sources or configuration snapshots. 它与 Lock Fairness and Starvation、Coroutines、Software Transactional Memory 等概念经常一起出现,用于解决相近的工程问题。理解它的适用场景,有助于在多种备选方案之间做出有依据的取舍,而不是凭直觉选择技术方案。
常见误解
- a seqlock gives readers a consistent but potentially old value on retry
- seqlocks are safe for pointers to freed memory
需要避免的反模式
- exposing mutable pointers through a seqlock without lifetime protection
- using a seqlock where writers are as frequent as readers
实践要点
- Writers increment the counter before and after modifying the data, and readers sample the counter before and after reading and retry when it changed or was odd.
- Readers never block writers, which is the appeal, but they can starve under heavy write pressure because each retry restarts from the beginning.
- The pattern assumes readers only need a consistent snapshot and do not dereference memory a writer might reclaim, so it must not be used for owning pointers.
参考来源
https://en.wikipedia.org/wiki/Seqlock
代码示例
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)
}
use arc_swap::ArcSwap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
struct Config {
max_connections: usize,
feature_flag: bool,
}
struct ConfigStore {
current: ArcSwap<Config>,
}
impl ConfigStore {
fn new(initial: Config) -> Self {
Self {
current: ArcSwap::from_pointee(initial),
}
}
fn load(&self) -> Arc<Config> {
self.current.load_full()
}
fn reload(&self, next: Config) -> Arc<Config> {
self.current.swap(Arc::new(next))
}
}
fn main() {
let store = ConfigStore::new(Config {
max_connections: 8,
feature_flag: false,
});
let snapshot = store.load();
let previous = store.reload(Config {
max_connections: 16,
feature_flag: true,
});
println!("previous: {:?}", previous);
println!("snapshot: {:?}", snapshot);
println!("current: {:?}", store.load());
assert_eq!(store.load().max_connections, 16);
}