@phper
2018-03-13T18:05:46.000000Z
字数 3278
阅读 3585
Golang
原文:https://golangbot.com/mutex/
欢迎访问 Golang 系列教程中的第25章。
在本教程中, 我们将了解互斥体。我们还将学习如何使用互斥体和信道来解决竞争条件.
在跳转到互斥体之前, 必须了解并发编程中关键节的概念。当一个程序同时运行时, 修改共享资源的代码部分不应同时由多个Goroutines访问。修改共享资源的这部分代码称为 "关键节"。例如, 假设我们有一些代码将变量 x 递增1。
x = x + 1
只要上面这段代码是由一个 Goroutine 访问的, 就不应该有任何问题。
让我们看看为什么当有多个 Goroutines 同时运行时, 代码会失败。为了简单起见, 假设我们有 2 Goroutines 同时运行上面的代码行。
在内部, 上述代码行将由系统执行以下步骤 (有更多的技术细节涉及寄存器, 如何添加工作等, 但为了本教程, 让我们假设, 这是三步骤),
获取 x 的当前值
计算 x + 1
将步骤2中的计算值分配给 x
当这三步仅由一个 Goroutine 执行时, 一切都很好。
让我们讨论一下当 2 Goroutines 同时运行此代码时会发生什么。下图描述了两个 Goroutines 同时访问代码x = x + 1行时可能发生的情况。
我们假设 x 的初始值为0。Goroutine 1获取 x 的初始值, 计算 x + 1, 然后在将计算值赋给 x 之前, 系统上下文切换到Goroutine 2。现在Goroutine 2获取x (仍然是0) 的初始值, 计算x + 1。在此之后, 系统上下文再次切换到Goroutine 1。现在, Goroutine 1将其计算值1分配给x , 因此 x 变为1。然后, Goroutine 2再次开始执行, 然后将它的计算值分配给它, 这再次是1到x , 因此x是1 , 在两个 Goroutines 执行之后。
现在让我们来看看可能发生的情况的不同情况。
在上述方案中, Goroutine 1开始执行并完成其所有三步骤, 因此 x 的值变为1。然后Goroutine 2开始执行。现在x的值为 1, 当Goroutine 2完成执行时, x的值为2.
因此, 从这两种情况可以看出, x 的最终值是1或2 , 具体取决于上下文切换的发生方式。此类不理想的情况下, 程序的输出取决于 Goroutines 的执行顺序, 称为争用条件.
在上述情况下, 如果仅允许一个 Goroutine 访问代码在任何时间点的关键部分, 则可以避免使用争用条件。这是通过使用互斥体实现的。
互斥体用于提供锁定机制, 以确保在任何时间点, 只有一个 Goroutine 运行关键代码段, 以防止竞争条件发生。
互斥体在sync包中可用。Mutex上定义了两种方法, 即锁定和解锁。在调用Lock和Unlock之间存在的任何代码都将仅由一个 Goroutine 执行, 从而避免了争用条件。
mutex.Lock()
x = x + 1
mutex.Unlock()
在上面的代码中, x = x + 1将只在任何时间点执行一个 Goroutine, 从而防止争用条件。
如果一个 Goroutine 已经持有锁, 如果新 Goroutine 试图获取锁, 则新的 Goroutine 将被阻止, 直到互斥锁被解除锁定。
具有竞赛条件的程序
在本节中, 我们将编写一个具有竞争条件的程序, 在即将进行的部分中, 我们将修正竞争条件。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x)
}
在上面的程序中, 7 行中的increment函数将x的值递增1 , 然后调用WaitGroup上的Done()通知其完成。
从上面的程序15行中生成1000个incrementGoroutines。当多个 Goroutines 尝试同时访问 x 的值时, 在尝试递增 x 为8行时, 会同时运行这些 Goroutines 和争用条件。
请在您的本地运行此程序, 因为在playground上是确定性的, 竞争条件不会在playground上发生.在本地计算机中多次运行此程序, 您可以看到由于竞争条件的不同, 每次输出都会有所不同。我遇到的一些输出是final value of x 941、final value of x 928、final value of x 922等等。
在上面的程序中, 我们产卵 1000 Goroutines。如果每个值的增量为 x 1, 则最终所需的 x 值应为1000。在本节中, 我们将使用互斥体修复上面的程序中的竞争条件。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
Mutex是一个结构类型, 我们在15行中创建了Mutex类型的零值变量m 。在上面的程序中, 我们更改了increment函数, 以便在m.Lock()和m.Unlock()之间递增 x x = x + 1的代码。现在, 这个代码是无效的任何种族条件, 因为只有一个 Goroutine 被允许执行这段代码在任何时间点。
现在, 如果这个程序运行, 它将输出
final value of x 1000
在18行中传递互斥锁的地址非常重要。如果互斥体通过值传递而不是传递地址, 则每个 Goroutine 都将有其自己的互斥副本, 并且仍会出现争用条件。
我们也可以用通道来解决竞争条件。让我们看看这是怎么做的。
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x)
}
在上面的程序中, 我们创建了容量为1的缓冲通道, 并将其传递给18行中的incrementGoroutine。此缓冲通道用于确保只有一个 Goroutine 访问代码的关键部分 (递增 x)。这是通过在x递增之前将true传递到8行中的缓冲通道来完成的。由于缓冲通道具有1的容量, 因此尝试写入此通道的所有其他 Goroutines 将被阻止, 直到在10行中递增 x 后从该通道读取该值。实际上, 这只允许一个 Goroutine 访问临界区。
该程序还打印
final value of x 1000
我们使用互斥和信道解决了竞争条件问题。那么, 我们如何决定什么时候使用。答案在于你试图解决的问题。如果您试图解决的问题是更好地适合互斥体, 则继续使用互斥体。如果需要, 请毫不犹豫地使用互斥锁。如果问题似乎更适合于渠道, 那么就使用它:)。
大多数新手尝试用一个通道解决每个并发问题, 因为它是语言的一个很酷的特性。这是错误的。该语言使我们可以选择使用互斥体或信道, 并且在选择二者中都没有错误。
在通常情况下, 当 Goroutines 需要相互通信和互斥时, 只有一个 Goroutine 应该访问代码的关键部分。
对于我们上面解决的问题, 我更愿意使用互斥体, 因为这个问题不需要 goroutines 之间的任何通信。因此, 互斥体将是一个自然的契合。
我的建议是选择这个问题的工具, 不要尝试去适应这个工具的问题:)
这使我们结束了本教程。