有人可以解释互斥锁中的术语"invariants"吗?



我已经读了Addison Wesley的书《Go编程语言》好几遍了,但在第265页第9章中,我仍然遇到了一些问题,其中谈到了同步。Mutex。

上面写着:

Go的互斥对象不是可重入对象是有充分理由的。互斥锁的目的是确保在程序执行过程中,共享变量的某些不变量在关键点得到维护。不变量之一是";没有goroutine正在访问共享变量";,但是可能存在特定于互斥体保护的数据结构的附加不变量。当goroutine获取互斥锁时,它可能会假设invariant持有。当它持有锁时,它可能会更新共享变量,从而暂时违反不变量。然而,当它释放锁时,它必须保证顺序已经恢复,不变量再次保持不变。尽管可重入互斥可以确保没有其他goroutine访问共享变量,但它不能保护这些变量的附加不变量。

我不是以英语为母语的人,我不确定术语不变量的含义。我只是认为它不会改变。但是,我仍然不能完全理解这一段的要点。有人能向我解释吗?如果除了没有goroutine访问变量之外还有其他不变量,这些是什么?

我不是以英语为母语的人,我不确定术语不变量的含义。我只是认为它不会改变。

请参阅marco.m的评论,该评论链接到"什么是不变量?"?了解更多关于不变量的信息。

在Go中,或者在任何具有并发性的语言中,我们可能需要互斥体的原因是,试图维护某个不变量的程序可能会故意允许该不变量被短暂违反。在一种没有并发性的编程语言中(因此不是Go),我们可能会有这样的代码:

// invariants: bestThing is the best thing (according to goodness ranking) in
// allThings; allThings[0] is always the initial badThing.
var bestThing Thing = badThing
var allThings []Thing = { badThing }
func addThing(t Thing) {
allThings = append(allThings, t)
// now, if t is better than the best thing so far, set bestThing = t
if goodness(t) > goodness(bestThing) {
bestThing = t
}
}

由于多种原因(例如全局变量),这是一个糟糕的代码,但是,只要我们的语言没有并发性,我们就会注意到addThing通过向allThings添加一些内容并在需要时更新bestThing来保持不变。

不过,如果我们在语言中添加并发addThing本身可能会中断。假设在一个线程/goroutine/whathing中,我们用一个非常好的东西调用addThing,在另一个线程中,我们使用另一个不同的、非常好的事情调用addThing。这些线程或goroutine中的一个或我们称之为并行运算符的任何一个开始修改allThings表和bestThing变量。另一个也一样。我们可能:

  • 由于在allThings中存储了错误的值而丢失了其中一个东西;和/或
  • 把第二好的东西评为最好的东西

因为不变量本身在一开始就被破坏了(使用allThings = append(allThings, t),它在一个全局变量上写入,并且只在函数返回时恢复(检查了优度并更新了最佳全局变量)。我们可以——笨拙地——用互斥锁修复这个问题:

func addThing(t Thing) {
someLock.Lock()
defer someLock.Unlock()
allThings = append(allThings, t)
// now, if t is better than the best thing so far, set bestThing = t
if goodness(t) > goodness(bestThing) {
bestThing = t
}
}

互斥锁确保,如果两个不同的goroutine(或它们是什么)进入addThing,其中一个会停止并等待另一个返回,然后再继续。继续的一个可以打断然后恢复不变量,另一个可以打破然后恢复不变量。

(这种笨拙的修复仍然不是很好:首先,我们现在需要用一个类似的互斥体来包装bestThing的每次使用,这样我们就不会在例程编写时读取它。但互斥体为我们提供了一个处理问题的工具。在真正的Go程序中的goroutines中使用这样的全局变量是"通过共享进行通信",而Go不鼓励这样做;"通过通信共享";通过频道。当然,数据结构需要重新设计才能做到这一点,例如,去掉这些简单的全局变量。这本身也是一件好事!)

Book提供了对互斥体的一种奇特的解释,但互斥体可以用任何奇特的词来解释。当您在互斥体上调用Lock()两次时,gorotine将被无限阻塞。但是,如果在调用之间调用Unlock,它将继续。这意味着如果两个goroutine同时调用互斥锁。Lock()被连续调用两次,一个例程将被阻塞,而另一个例程可以执行。当执行rotine完成读取或更改共享状态时,必须调用Unlock()开关,使阻塞的例程亮起绿灯才能访问共享状态。我们用Lock() Unlock()包围了关键代码,因此这些调用之间的代码不可能在多个gorotine上同时执行。让我们来看看一些代码,这些代码显示了互斥的有效和无效使用。

package main
import (
"sync"
"time"
)
func main() {
/// valid code
mut := sync.Mutex{} // mutex has to e used on both ends
sharedState := 0
go func() {
//mut := mut // this is invalid operation, it creates copy of a lock, so locking one does not affect another
for i := 0; i < 100000; i++ {
mut.Lock()
// if lock is not present one routine can modify data while other is reading, in that moment
// datarace happens
sharedState = sharedState + 1
mut.Unlock()
}
}()
// not locking on either side is also invalid and panic will happen
for i := 0; i < 100000; i++ {
mut.Lock()
sharedState = sharedState + 1
mut.Unlock()
}
time.Sleep(time.Second) // this is just simplification, to make sure all routines finished we usually use wait group or channels
if sharedState != 200000 {
panic("this will never happen")
}
/// invalid code
sharedState = 0
go func() {
for i := 0; i < 100000; i++ {
// lock is not present on both sides, locking just one side has no effect
sharedState = sharedState + 1
}
}()
// not locking on either side is also invalid and panic will happen
for i := 0; i < 100000; i++ {
mut.Lock()
sharedState = sharedState + 1
mut.Unlock()
}
time.Sleep(time.Second)
if sharedState == 200000 {
panic("there is very little chance this panic will happen, newer assume it will")
}
}

锁定可以确保代码不会同时执行,它不会使互斥调用之间的操作成为原子操作,所以仅仅锁定写入程序是不够的。虽然多个阅读器可以共存,但sync.RWMutex存在的原因

最新更新