Skip to content

Latest commit

 

History

History
384 lines (297 loc) · 12.4 KB

并发安全.md

File metadata and controls

384 lines (297 loc) · 12.4 KB

BTreeG[T] 的实现中,通过多种机制确保了并发安全性(线程安全)。以下是详细的说明:

1. 使用 sync.RWMutex 进行同步控制

1.1 读写锁 (sync.RWMutex)

  • 定义与初始化

    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:布尔值,决定是否启用锁机制。如果 lockstrue,则在所有需要同步的操作中会使用锁;否则,跳过锁的使用,以提升性能,但需要确保外部调用者自行管理并发安全。

1.2 锁的使用方式

  • 加锁与解锁方法

    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)
      • 如果 lockstrue,根据 write 参数决定是获取写锁 (mu.Lock()) 还是读锁 (mu.RLock())。
      • 返回 tr.locks,用于判断调用者是否需要在操作完成后释放锁。
    • unlock(write bool)
      • 如果 lockstrue,根据 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 同时访问时,读操作可以并行进行,而写操作会独占访问,避免数据竞争。

2. Copy-On-Write (COW) 机制

2.1 隔离 ID (isoid)

  • 目的

    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
      • 如果 copyItemsisoCopyItemstrue,则对每个 item 进行深拷贝,确保数据的完整性。
    • isoLoad 方法
      • 在需要修改节点时(mut == true),检查节点的 isoid 是否与当前树的 isoid 一致。
      • 如果不一致,调用 copy 方法复制节点,确保修改不会影响到其他共享副本。

2.2 复制树 (Copy / IsoCopy)

  • 复制操作

    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 的优势
      • 复制操作非常快速,因为它只复制了树的结构,而节点的数据只有在后续修改时才会被实际复制。
      • 多个树副本可以共享相同的节点数据,节省内存。

3. 迭代器的并发安全

3.1 迭代器结构

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:布尔值,指示迭代器是否持有锁。
    • seekedatstartatend:用于跟踪迭代器的位置状态。
    • stack:用于保存从根节点到当前节点的路径,支持 NextPrev 操作。
    • item:当前迭代器指向的项目。

3.2 迭代器方法中的锁使用

  • 创建迭代器

    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:返回一个只读的迭代器,不需要修改树,因此 mutfalse
    • IterMut:返回一个可变的迭代器,可能会修改树,因此 muttrue
    • 在创建迭代器时,根据 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 方法:在完成迭代后,释放迭代器持有的锁,确保其他操作可以继续进行。

4. 读写操作的并发控制

4.1 读操作

  • 查找 (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) 获取读锁。

4.2 写操作

  • 插入 (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 等)

    与插入类似,删除操作需要修改树的结构,因此也需要获取 写锁

5. NoLocks 选项

  • 用途

    通过在创建 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
    }
    • 效果
      • 如果 NoLocksfalse(默认),则 lockstrue,启用锁机制。
      • 如果 NoLockstrue,则 locksfalse,跳过锁的获取与释放。

6. Copy-On-Write 与 并发安全的结合

  • 读写分离

    通过 Copy-On-Write 机制,多个读操作可以并行访问相同的节点,而写操作只会复制需要修改的节点,避免了读写之间的相互干扰。

  • 一致性保证

    • 读操作:通过获取读锁,并访问节点的当前快照,确保在读过程中树的结构不会被修改。
    • 写操作:通过获取写锁,并使用 Copy-On-Write 复制需要修改的节点,确保修改后的树结构对其他并发读操作不可见,直到写操作完成。

7. 迭代器的锁管理

  • 持有锁

    当创建迭代器时,迭代器会根据 mut 参数决定是否获取锁。这确保了在迭代过程中,树的结构不会被其他写操作修改,保持迭代的稳定性。

  • 释放锁

    迭代器在完成操作后必须调用 Release 方法,释放持有的锁,避免死锁和资源泄漏。

总结

BTreeG[T] 通过以下机制确保了并发安全:

  1. 读写锁 (sync.RWMutex)

    • 允许多个读操作并行进行。
    • 写操作独占访问,防止数据竞争。
  2. Copy-On-Write (COW) 机制

    • 通过 isoid 标识符和节点复制,确保在修改树结构时不会影响到正在进行的读操作。
    • 提高了读写分离的效率,尤其适用于读多写少的场景。
  3. 可配置的锁机制 (NoLocks 选项)

    • 允许根据具体需求启用或禁用锁,以在不同的使用场景下优化性能。
  4. 迭代器的锁管理

    • 迭代器在遍历过程中持有相应的锁,确保遍历的一致性和安全性。

通过这些机制,BTreeG[T] 能够在高并发环境下安全、高效地运行,既保证了数据的一致性,又提供了良好的性能表现。