在 BTreeG[T]
的实现中,通过多种机制确保了并发安全性(线程安全)。以下是详细的说明:
-
定义与初始化:
type BTreeG[T any] struct { isoid uint64 // 树的“隔离ID”,用于COW判断 mu *sync.RWMutex // 读写锁 root *node[T] count int locks bool // 是否启用锁 copyItems bool // 是否需要在插入/复制时对 items 做深拷贝 isoCopyItems bool // 是否需要特殊的 IsoCopy less func(a, b T) bool empty T // 零值 max int // 每个节点最大元素数 min int // 每个节点最小元素数 }
mu
:指向sync.RWMutex
的指针,用于控制对 B-Tree 的并发访问。locks
:布尔值,决定是否启用锁机制。如果locks
为true
,则在所有需要同步的操作中会使用锁;否则,跳过锁的使用,以提升性能,但需要确保外部调用者自行管理并发安全。
-
加锁与解锁方法:
func (tr *BTreeG[T]) lock(write bool) bool { if tr.locks { if write { tr.mu.Lock() } else { tr.mu.RLock() } } return tr.locks } func (tr *BTreeG[T]) unlock(write bool) { if tr.locks { if write { tr.mu.Unlock() } else { tr.mu.RUnlock() } } }
lock(write bool)
:- 如果
locks
为true
,根据write
参数决定是获取写锁 (mu.Lock()
) 还是读锁 (mu.RLock()
)。 - 返回
tr.locks
,用于判断调用者是否需要在操作完成后释放锁。
- 如果
unlock(write bool)
:- 如果
locks
为true
,根据write
参数决定是释放写锁 (mu.Unlock()
) 还是读锁 (mu.RUnlock()
)。
- 如果
-
在各方法中的应用:
大部分公共方法在执行前都会调用
lock
方法获取相应的锁,操作完成后通过defer unlock
释放锁。例如:func (tr *BTreeG[T]) SetHint(item T, hint *PathHint) (prev T, replaced bool) { if tr.locks { tr.mu.Lock() prev, replaced = tr.setHint(item, hint) tr.mu.Unlock() } else { prev, replaced = tr.setHint(item, hint) } return prev, replaced }
或者更常见的写法:
func (tr *BTreeG[T]) getHint(key T, hint *PathHint, mut bool) (T, bool) { if tr.lock(mut) { defer tr.unlock(mut) } // ... 方法主体 ... }
通过这种方式,确保在多 goroutine 同时访问时,读操作可以并行进行,而写操作会独占访问,避免数据竞争。
-
目的:
COW 机制的核心在于通过
isoid
来标识不同的 B-Tree 副本。当需要对树进行修改时,首先检查节点的isoid
是否与当前树的isoid
一致。如果不一致,表示该节点可能被其他副本共享,此时需要复制节点,以确保修改不会影响到其他副本。 -
实现:
type BTreeG[T any] struct { isoid uint64 // 树的“隔离ID”,用于COW判断 // ... 其他字段 ... } type node[T any] struct { isoid uint64 // ... 其他字段 ... } func (tr *BTreeG[T]) copy(n *node[T]) *node[T] { n2 := new(node[T]) n2.isoid = tr.isoid n2.count = n.count n2.items = make([]T, len(n.items), cap(n.items)) copy(n2.items, n.items) if tr.copyItems { for i := 0; i < len(n2.items); i++ { n2.items[i] = ((interface{})(n2.items[i])).(copier[T]).Copy() } } else if tr.isoCopyItems { for i := 0; i < len(n2.items); i++ { n2.items[i] = ((interface{})(n2.items[i])).(isoCopier[T]).IsoCopy() } } if !n.leaf() { n2.children = new([]*node[T]) *n2.children = make([]*node[T], len(*n.children), tr.max+1) copy(*n2.children, *n.children) } return n2 } func (tr *BTreeG[T]) isoLoad(cn **node[T], mut bool) *node[T] { if mut && (*cn).isoid != tr.isoid { *cn = tr.copy(*cn) } return *cn }
copy
方法:- 创建节点
n
的副本n2
。 - 复制
items
和(如果不是叶子节点)children
。 - 如果
copyItems
或isoCopyItems
为true
,则对每个item
进行深拷贝,确保数据的完整性。
- 创建节点
isoLoad
方法:- 在需要修改节点时(
mut == true
),检查节点的isoid
是否与当前树的isoid
一致。 - 如果不一致,调用
copy
方法复制节点,确保修改不会影响到其他共享副本。
- 在需要修改节点时(
-
复制操作:
func (tr *BTreeG[T]) Copy() *BTreeG[T] { return tr.IsoCopy() } func (tr *BTreeG[T]) IsoCopy() *BTreeG[T] { if tr.lock(true) { defer tr.unlock(true) } tr.isoid = newIsoID() tr2 := new(BTreeG[T]) *tr2 = *tr tr2.mu = new(sync.RWMutex) tr2.isoid = newIsoID() return tr2 }
Copy
方法:- 调用
IsoCopy
方法进行树的复制。
- 调用
IsoCopy
方法:- 获取写锁,确保复制过程的原子性。
- 更新当前树的
isoid
,防止后续修改影响到复制后的树。 - 通过浅拷贝(
*tr2 = *tr
)复制树的结构。 - 为复制后的树分配新的
mu
(锁)和isoid
。 - 返回新的树副本
tr2
。
- Copy-On-Write 的优势:
- 复制操作非常快速,因为它只复制了树的结构,而节点的数据只有在后续修改时才会被实际复制。
- 多个树副本可以共享相同的节点数据,节省内存。
type IterG[T any] struct {
tr *BTreeG[T]
mut bool
locked bool
seeked bool
atstart bool
atend bool
stack0 [4]iterStackItemG[T]
stack []iterStackItemG[T]
item T
}
type iterStackItemG[T any] struct {
n *node[T]
i int
}
- 字段解释:
tr
:指向 B-Tree 的指针。mut
:布尔值,指示迭代器是否需要进行修改(可变迭代器)。locked
:布尔值,指示迭代器是否持有锁。seeked
、atstart
、atend
:用于跟踪迭代器的位置状态。stack
:用于保存从根节点到当前节点的路径,支持Next
和Prev
操作。item
:当前迭代器指向的项目。
-
创建迭代器:
func (tr *BTreeG[T]) Iter() IterG[T] { return tr.iter(false) } func (tr *BTreeG[T]) IterMut() IterG[T] { return tr.iter(true) } func (tr *BTreeG[T]) iter(mut bool) IterG[T] { var iter IterG[T] iter.tr = tr iter.mut = mut iter.locked = tr.lock(iter.mut) iter.stack = iter.stack0[:0] return iter }
Iter
:返回一个只读的迭代器,不需要修改树,因此mut
为false
。IterMut
:返回一个可变的迭代器,可能会修改树,因此mut
为true
。- 在创建迭代器时,根据
mut
参数决定是否获取锁,以确保迭代过程中树的结构不会被其他写操作修改。
-
释放迭代器:
func (iter *IterG[T]) Release() { if iter.tr == nil { return } if iter.locked { iter.tr.unlock(iter.mut) iter.locked = false } iter.stack = nil iter.tr = nil }
Release
方法:在完成迭代后,释放迭代器持有的锁,确保其他操作可以继续进行。
-
查找 (
Get
,GetHint
,GetAt
等):这些方法通常只需要读取树中的数据,因此会获取 读锁 (
mu.RLock()
)。多个读操作可以并行进行,不会相互阻塞。func (tr *BTreeG[T]) Get(key T) (T, bool) { return tr.getHint(key, nil, false) } func (tr *BTreeG[T]) getHint(key T, hint *PathHint, mut bool) (T, bool) { if tr.lock(mut) { defer tr.unlock(mut) } // ... 方法主体 ... }
mut
参数为false
时,调用lock(false)
获取读锁。
-
插入 (
Set
,SetHint
,Load
等):这些方法会修改树的结构或数据,因此需要获取 写锁 (
mu.Lock()
)。写锁是独占的,确保在写操作进行时,其他读写操作被阻塞,避免数据不一致。func (tr *BTreeG[T]) Set(item T) (T, bool) { return tr.SetHint(item, nil) } func (tr *BTreeG[T]) SetHint(item T, hint *PathHint) (prev T, replaced bool) { if tr.locks { tr.mu.Lock() prev, replaced = tr.setHint(item, hint) tr.mu.Unlock() } else { prev, replaced = tr.setHint(item, hint) } return prev, replaced }
mut
参数为true
时,调用lock(true)
获取写锁。
-
删除 (
Delete
,DeleteHint
,DeleteAt
等):与插入类似,删除操作需要修改树的结构,因此也需要获取 写锁。
-
用途:
通过在创建 B-Tree 时设置
NoLocks
选项,可以禁用锁机制。这在某些场景下(如单线程环境或外部已经保证并发安全)可以提升性能。 -
实现:
type Options struct { Degree int NoLocks bool // 禁用锁机制 } func NewBTreeGOptions[T any](less func(a, b T) bool, opts Options) *BTreeG[T] { tr := new(BTreeG[T]) tr.isoid = newIsoID() tr.mu = new(sync.RWMutex) tr.locks = !opts.NoLocks // 如果 NoLocks 为 true,则 tr.locks 为 false tr.less = less tr.init(opts.Degree) return tr }
- 效果:
- 如果
NoLocks
为false
(默认),则locks
为true
,启用锁机制。 - 如果
NoLocks
为true
,则locks
为false
,跳过锁的获取与释放。
- 如果
- 效果:
-
读写分离:
通过 Copy-On-Write 机制,多个读操作可以并行访问相同的节点,而写操作只会复制需要修改的节点,避免了读写之间的相互干扰。
-
一致性保证:
- 读操作:通过获取读锁,并访问节点的当前快照,确保在读过程中树的结构不会被修改。
- 写操作:通过获取写锁,并使用 Copy-On-Write 复制需要修改的节点,确保修改后的树结构对其他并发读操作不可见,直到写操作完成。
-
持有锁:
当创建迭代器时,迭代器会根据
mut
参数决定是否获取锁。这确保了在迭代过程中,树的结构不会被其他写操作修改,保持迭代的稳定性。 -
释放锁:
迭代器在完成操作后必须调用
Release
方法,释放持有的锁,避免死锁和资源泄漏。
BTreeG[T]
通过以下机制确保了并发安全:
-
读写锁 (
sync.RWMutex
):- 允许多个读操作并行进行。
- 写操作独占访问,防止数据竞争。
-
Copy-On-Write (COW) 机制:
- 通过
isoid
标识符和节点复制,确保在修改树结构时不会影响到正在进行的读操作。 - 提高了读写分离的效率,尤其适用于读多写少的场景。
- 通过
-
可配置的锁机制 (
NoLocks
选项):- 允许根据具体需求启用或禁用锁,以在不同的使用场景下优化性能。
-
迭代器的锁管理:
- 迭代器在遍历过程中持有相应的锁,确保遍历的一致性和安全性。
通过这些机制,BTreeG[T]
能够在高并发环境下安全、高效地运行,既保证了数据的一致性,又提供了良好的性能表现。