当一个程序并发运行时,修改共享资源的部分代码不应该被多个Goroutines同时访问
修改共享资源的这段代码被称为临界区
使用互斥锁,限定临界区只能同时由一个线程持有
互斥锁
Mutex用于提供一种锁定机制,以确保在任何时间点只有一个Goroutine正在运行代码的关键部分,以防止竞态条件
互斥锁在同步中可用。Lock和Unlock
mutex.Lock()
x = x+1
mutex.Unlock()
在上面的代码中,x = x + 1在任何时间点都只会由一个 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)
}
在上面的程序中,increment函数在第7行将x增加1。然后调用WaitGroup的Done()来通知其完成。
我们从第 10 行生成了 1000 个增量 Goroutine。这些 Goroutines 中的每一个都是并发运行的,并且当尝试增加 x 时会出现竞争条件。当多个 Goroutine 试图同时访问 x 的值时,在第8行尝试增加 x 时发生竞争条件。
请在您本机运行此程序,因为playground是确定性的,并且竞争条件不会发生在playground上。在你的本地机器上多次运行这个程序,你可以看到每次的输出都会因为竞争条件而不同。
使用互斥锁解决竞争条件
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)
}
Mutexm是一种结构类型,我们在第 15 行创建了一个类型为Mutex的零值的变量m。 在上面的程序中,我们改变了increment函数,使 x = x + 1增加 x 的代码在m.Lock()和m.Unlock()之间。现在这段代码没有任何竞争条件,因为在任何时间点只允许一个 Goroutine 执行这段代码。
现在如果这个程序运行,它会输出:final value of x 1000
在第 18 行中传递互斥体的地址很重要。 如果互斥量是按值传递而不是传递地址,每个 Goroutine 都会有自己的互斥量副本,仍然会出现竞态条件。
使用channel解决竞争条件
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)
}
在上面的程序中,我们创建了一个缓冲 channel 并指定容量为 1,并将其传递给了18行中的 increment Goroutine。这个缓冲 channel 用于确保只有一个 Goroutine 访问增加 x 的代码的关键部分。这是通过在 x 增加之前将 true 传递给缓冲 chennel 来实现的,由于缓冲 channel 的容量为 1,所以所有的其他试图写入这个 channel 的 Goroutine 都会被阻塞。第10行直到将 x 增加之后从 channel 读取该值。这实际上只允许一个 Goroutine 访问临界区。
该程序打印:final value of x 1000
互斥与通道
我们已经使用互斥锁和通道解决了竞争条件问题。那么我们如何决定什么时候使用呢?答案在于您要解决的问题。如果您要解决的问题更适合互斥锁,那么请继续使用互斥锁。如果需要,请不要犹豫使用互斥锁。如果问题似乎更适合渠道,请使用它:)。
大多数 Go 新手尝试使用通道来解决每个并发问题,因为它是该语言的一个很酷的特性。这是错误的。该语言为我们提供了使用 Mutex 或 Channel 的选项,选择其中任何一个都没有错。
通常当 Goroutine 需要相互通信时使用通道,当只有一个 Goroutine 应该访问代码的关键部分时使用互斥锁。
对于我们上面解决的问题,我更喜欢使用互斥锁,因为这个问题不需要 goroutine 之间的任何通信。因此,互斥锁将是自然而然的选择。