MIT 6.824 - Lab 2 (2): Raft Locking Advice
Raft Locking Advice 提供了些关于如何在 Lab 2
中使用锁的建议。
规则1
只要有多个 goroutine
访问同一份数据,并且至少有一个 goroutine
会修改数据,那么就需要对数据加锁保护。建议在测试时开启 Go
的竞争检测(添加 -race
标记)来识别这类问题。
规则2
如果有多个数据需要作为一个整体被修改,为了避免其他的 goroutine
看到部分数据更新而造成不正确的行为,此时也需要加锁。例如:
1 | rf.mu.Lock() |
上面的代码需要同时更新 rf.currentTerm
和 rf.state
,如果不加锁其他 goroutine
有可能看到更新后的任期,但是节点状态还未更新。同时,其他任何地方用到 rf.currentTerm
或者 rf.state
的地方也必须先持有锁,一是保证可见性,二是避免在多处同时修改 rf.currentTerm
或者 rf.state
。
规则3
如果需要对某个数据做一系列读操作(或者读写混合),那么为了避免其他 goroutine
在中途修改数据,就需要对这一系列操作加锁。例如:
1 | rf.mu.Lock() |
上面的代码是典型的 如果满足某个条件,那么就执行 xxx
场景。如果不加锁,可能其他的 goroutine
将 rf.currentTerm
更新后,当前 goroutine
会将 rf.currentTerm
重置为 args.Term
,在 Raft
中有可能造成任期倒退。
在真实的 Raft
代码中加锁的粒度可能会更大,例如可能在整个 RPC handler
处理期间都持有锁。
规则4
不建议在等待某个事件时持有锁,例如从 channel
中读取数据,向 channel
发送数据,计时器等待,调用 time.Sleep()
,或者发送一个 RPC
请求并等待响应结果。因为有可能造成死锁,文中举了两个节点互发 RPC
请求并希望获取对方持有的锁的例子,这是个典型的死锁场景。
又或者某个 goroutine
先持有锁,但是使用 time.Sleep
来等待某个条件发生,其他的 goroutine
由于无法获取锁从而使得等待的条件永远无法成立,这个时候应该用 sync.Cond
:
1 | mu.Lock() |
规则5
当释放锁然后重新获取锁之后,某些释放锁之前成立的条件可能此时已经不成立。例如下面的候选节点获取选票的实现是不正确的:
1 | rf.mu.Lock() |
在每个 goroutine
中,重新获取锁后拿到的任期可能已经不是当初的任期。这里需要将 goroutine
中的 rf.currentTerm
提取到循环之外作为一个变量,然后在 goroutine
中访问这个变量。另外,在 Call
执行完成后,也需要再次获取锁并检查 rf.currentTerm
或其他变量是否还满足条件,例如需要检查下当前的任期是否还是最初的任期,如果不是那说明又开启了一轮选主或者已经有其他节点成为了主节点。