Desc:Go sync 包的使用方法,sync.Mutex,sync.RMutex,sync.Once,sync.Cond,sync.Waitgroup
尽管 Golang 推荐通过 channel 进行通信和同步,但在实际开发中 sync 包用得也非常的多。另外 sync 下还有一个 atomic 包,提供了一些底层的原子操作(这里不做介绍)。本篇文章主要介绍该包下的锁的一些概念及使用方法。
整个包都围绕这 Locker 进行,这是一个 interface:
type Locker interface {
Lock()
Unlock()
}
只有两个方法,Lock()
和 Unlock()
。
另外该包下的对象,在使用过之后,千万不要复制。
有许多同学不理解锁的概念,下面会一一介绍到:
在并发的情况下,多个线程或协程同时去修改一个变量,可能会出现如下情况:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
// 启动 100 个协程,需要足够大
// var lock sync.Mutex
for i := 0; i < 100; i++ {
go func(idx int) {
// lock.Lock()
// defer lock.Unlock()
a += 1
fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
}
// 等待 1s 结束主程序
// 确保所有协程执行完
time.Sleep(time.Second)
}
观察打印结果,是否出现 a 的值是相同的情况(未出现则重试或调大协程数),答案:是的。
显然这不是我们想要的结果。出现这种情况的原因是,协程依次执行:从寄存器读取 a 的值 -> 然后做加法运算 -> 最后写会寄存器。试想,此时一个协程取出 a 的值 3,正在做加法运算(还未写回寄存器)。同时另一个协程此时去取,取出了同样的 a 的值 3。最终导致的结果是,两个协程产出的结果相同,a 相当于只增加了 1。
所以,锁的概念就是,我正在处理 a(锁定),你们谁都别和我抢,等我处理完了(解锁),你们再处理。这样就实现了,同时处理 a 的协程只有一个,就实现了同步。
把上面代码里的注释取消掉再试下。
每次确定都是100,这个很像Java和数据库的悲观锁
什么是互斥锁?它是锁的一种具体实现,有两个方法:
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
在首次使用后不要复制该互斥锁。对一个未锁定的互斥锁解锁将会产生运行时错误。
一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定)。
使用例子:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan struct{}, 2)
var l sync.Mutex
go func() {
l.Lock()
defer l.Unlock()
fmt.Println("goroutine1: 我会锁定大概 2s")
time.Sleep(time.Second * 2)
fmt.Println("goroutine1: 我解锁了,你们去抢吧")
ch <- struct{}{}
}()
go func() {
fmt.Println("groutine2: 等待解锁")
l.Lock()
defer l.Unlock()
fmt.Println("goroutine2: 哈哈,我锁定了")
ch <- struct{}{}
}()
// 等待 goroutine 执行结束
for i := 0; i < 2; i++ {
s := <-ch
fmt.Println(s)
}
}
WaitGroup 用于等待一组 goroutine 结束,用法很简单。它有三个方法:
func (wg *WaitGroup) Add(delta int) //Add(n) 把计数器设置为n ,
func (wg *WaitGroup) Done() //Done() 每次把计数器-1
func (wg *WaitGroup) Wait() //wait() 会阻塞代码的运行,直到计数器地值减为0
package main
import (
"sync"
"fmt"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(100)
for i := 0; i < 100; i++ {
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
sync.Once.Do(f func())是一个能保证once只执行一次,无论你是否更换once.Do(xx)这里的方法,这个sync.Once块只会执行一次。
package main
import (
"fmt"
"sync"
"time"
)
var once sync.Once
func main() {
for i, v := range make([]string, 10) {
once.Do(onces)
fmt.Println("count:", v, "---", i)
}
for i := 0; i < 10; i++ {
go func() {
once.Do(onced)
fmt.Println("213")
}()
}
time.Sleep(4000)
}
func onces() {
fmt.Println("onces")
}
func onced() {
fmt.Println("onced")
}