【问题标题】:Could someone explain the term "invariants" in mutex locking?有人可以解释互斥锁中的“不变量”一词吗?
【发布时间】:2021-05-17 13:49:42
【问题描述】:

我已经多次阅读 Addison-Wesley 的书,Go 编程语言,但我仍然在第 9 章第 265 页中遇到了一些问题,其中谈到了 sync.Mutex。

上面写着:

Go 的互斥体不能重入是有充分理由的。互斥锁的目的是确保共享变量的某些不变量在程序执行期间保持在关键点。其中一个不变量是“没有 goroutine 正在访问共享变量”,但可能存在特定于互斥锁保护的数据结构的其他不变量。当一个 goroutine 获得一个互斥锁时,它可能会假设 invirants 持有。当它持有锁时,它可能会更新共享变量,以便暂时违反不变量。但是,当它释放锁时,它必须保证顺序已经恢复并且不变量再次保持。尽管可重入互斥锁可以确保没有其他 goroutine 访问共享变量,但它不能保护这些变量的附加不变量。

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

【问题讨论】:

标签: go concurrency locking mutex


【解决方案1】:

我不是以英语为母语的人,我不确定术语不变量的含义。我只是把它当作不会改变的东西。

有关不变量的更多信息,请参阅链接到 What is an invariant?marco.m's comment

我们可能在 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/whatever 中,我们用一个非常好的东西调用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 的每次使用,这样我们就不会在例程编写它时读取它。但是mutex 为我们提供了一个工具来解决这个问题。在真正的 Go 程序的 goroutine 中使用像这样的全局变量是“通过共享进行通信”,而 Go 不鼓励通过通道“通过通信进行共享”。当然数据结构需要重新设计才能做到这一点,例如摆脱这些简单的全局变量。这本身也是一件好事!)

【讨论】:

    【解决方案2】:

    本书对互斥体提供了非常奇特的解释,但互斥体可以不用任何花哨的词来解释。当你在 mutex 上调用 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 存在的原因

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-01-10
      • 1970-01-01
      • 1970-01-01
      • 2020-09-02
      • 2011-03-17
      • 2018-05-23
      • 1970-01-01
      相关资源
      最近更新 更多