临界区
当程序并发地运行时,多个goroutine不应该同时访问那些修改共享资源的代码。这些修改共享资源的代码称为临界区。
Mutex
Mutex用于提供一种加锁机制,可确保在某一时刻只有一个协程在临界区运行,以防止出现竞态条件。Mutex可以在sync包内找到。Mutex定义了两个方法:Lock
和Unlock
。所有在Lock和Unlock之间的代码,都只能由一个Go协程执行,避免了竞态条件。如果有一个Go协程已经持有了锁,当其它协程试图获得该锁时,这些协程会被阻塞,直到Mutex解除锁定为止。
mutex.Lock()
{
// 放置会出现竞态条件代码
x = x + 1
}
mutex.Unlock()
含有竞态条件的程序
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1 // 当多个并发的协程试图访问x的值,就会发生竞态条件
wg.Done()
}
func main() {
w := sync.WaitGroup{}
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x:", x)
}
使用Mutex
这里使用Mutex修复上述竞态条件问题:
package main
import (
"fmt"
"sync"
)
var x = 0
// 注意这里必须传递Mutex的地址
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1 // 任何时刻都只允许一个协程执行这段代码
m.Unlock()
wg.Done()
}
func main() {
w := sync.WaitGroup{}
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)
}
使用信道处理竞态条件
这里使用Channel修复上述竞态条件问题:
package main
import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
ch <- true
// 由于缓冲信道的容量为1,所以任何其它协程试图写入该信道时,
// 都会发生阻塞,直到x增加后,信道的值才会被读取,完成解锁。
x = x + 1
<-ch
wg.Done()
}
func main() {
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)
}
Mutex还是Channel?
Mutex和Channel都可以用来解决竞态条件问题。那么如何选择?
总体来说,当Go协程需要与其它协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用Mutex。就上述代码而言,使用Mutex更直观一些。
reference: