MIT 6.824 - Lab 2 (2): Raft Locking Advice

Raft Locking Advice 提供了些关于如何在 Lab 2 中使用锁的建议。

规则1

只要有多个 goroutine 访问同一份数据,并且至少有一个 goroutine 会修改数据,那么就需要对数据加锁保护。建议在测试时开启 Go 的竞争检测(添加 -race 标记)来识别这类问题。

规则2

如果有多个数据需要作为一个整体被修改,为了避免其他的 goroutine 看到部分数据更新而造成不正确的行为,此时也需要加锁。例如:

1
2
3
4
rf.mu.Lock()
rf.currentTerm += 1
rf.state = Candidate
rf.mu.Unlock()

上面的代码需要同时更新 rf.currentTermrf.state,如果不加锁其他 goroutine 有可能看到更新后的任期,但是节点状态还未更新。同时,其他任何地方用到 rf.currentTerm 或者 rf.state 的地方也必须先持有锁,一是保证可见性,二是避免在多处同时修改 rf.currentTerm 或者 rf.state

规则3

如果需要对某个数据做一系列读操作(或者读写混合),那么为了避免其他 goroutine 在中途修改数据,就需要对这一系列操作加锁。例如:

1
2
3
4
5
rf.mu.Lock()
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
}
rf.mu.Unlock()

上面的代码是典型的 如果满足某个条件,那么就执行 xxx 场景。如果不加锁,可能其他的 goroutinerf.currentTerm 更新后,当前 goroutine 会将 rf.currentTerm 重置为 args.Term,在 Raft 中有可能造成任期倒退。

在真实的 Raft 代码中加锁的粒度可能会更大,例如可能在整个 RPC handler 处理期间都持有锁。

规则4

不建议在等待某个事件时持有锁,例如从 channel 中读取数据,向 channel 发送数据,计时器等待,调用 time.Sleep(),或者发送一个 RPC 请求并等待响应结果。因为有可能造成死锁,文中举了两个节点互发 RPC 请求并希望获取对方持有的锁的例子,这是个典型的死锁场景。

又或者某个 goroutine 先持有锁,但是使用 time.Sleep 来等待某个条件发生,其他的 goroutine 由于无法获取锁从而使得等待的条件永远无法成立,这个时候应该用 sync.Cond

1
2
3
4
5
6
7
mu.Lock()

while (!someCondition) {
time.Sleep(time.Millisecond * 1000)
}

mu.Unlock()

规则5

当释放锁然后重新获取锁之后,某些释放锁之前成立的条件可能此时已经不成立。例如下面的候选节点获取选票的实现是不正确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
rf.mu.Lock()
rf.currentTerm += 1
rf.state = Candidate
for <each peer> {
go func() {
rf.mu.Lock()
args.Term = rf.currentTerm
rf.mu.Unlock()
Call("Raft.RequestVote", &args, ...)
// handle the reply...
} ()
}
rf.mu.Unlock()

在每个 goroutine 中,重新获取锁后拿到的任期可能已经不是当初的任期。这里需要将 goroutine 中的 rf.currentTerm 提取到循环之外作为一个变量,然后在 goroutine 中访问这个变量。另外,在 Call 执行完成后,也需要再次获取锁并检查 rf.currentTerm 或其他变量是否还满足条件,例如需要检查下当前的任期是否还是最初的任期,如果不是那说明又开启了一轮选主或者已经有其他节点成为了主节点。

参考