共享内存
我们知道,channel 可以在在多个 goroutine 之间进行通信,其实对于并发还有一种较为常用通信方式,那就是共享内存,这是因为每个协程函数里都可以访问到全局变量。首先我们来看一个例子:
var name string
func printName() {
log.Println("name is", name)
}
func main() {
name = "小明"
go printName() // 输出小明
go printName() // 输出小明
time.Sleep(time.Second)
name = "小红"
go printName() // 输出小红
go printName() // 输出小红
time.Sleep(time.Second)
}
可以看到在两个 goroutine 中我们都可以访问 name 这个变量,当修改它后,在不同的 goroutine 中都可以同时获取到最新的值。这就是一个最简单的通过共享变量(内存)的方式在多个 goroutine 进行通信的方式。
如上我们知道,对变量进行并行读问题不太大,但是对变量进行并行的写的时候如果不加锁就会出现问题:
func main() {
var (
wg sync.WaitGroup
numbers []int
)
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
numbers = append(numbers, i)
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("The numbers is", len(numbers))
}
上诉例子运行的之后,有的时候,输出的结果是是65,有的时候输出是80等等,反正输出的结果不固定,不符合预期结果100,可以看到当我们并发对同一个切片进行写操作的时候,会出现数据不一致的问题,这就是一个典型的共享变量的问题。
sync.Mutex互斥锁
针对这个问题我们可以使用 Mutex 锁(其它语言中也叫排他锁、写锁)来修复,sync.Mutex
一旦被锁住,其它的 Lock()
操作就无法再获取它的锁,只有通过 Unlock()
释放锁之后才能通过 Lock()
继续获取锁,从而保证数据的一致性。
另外需要注意,sync.Mutex
不区分读写锁,只有 Lock()
与 Lock()
之间才会导致阻塞的情况,如果在一个地方 Lock()
,在另一个地方不 Lock()
而是直接修改或访问共享数据,这对于 sync.Mutex
类型来说是允许的,因为 mutex
不会和 goroutine
进行关联。例如:
func main() {
var (
wg sync.WaitGroup
numbers []int
mux sync.Mutex
)
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
mux.Lock()
numbers = append(numbers, i)
mux.Unlock()
wg.Done()
}(i)
}
wg.Wait()
fmt.Println("The numbers is", len(numbers))
}
修改过后,我们再次运行代码,可以看到最后的 numbers 的数量始终是100个了。
可以使用 TryLock 来不阻塞判断是否被锁住
// serverAutoUnregisterLocker 防止本任务还没执行完成,下个任务又开始,拖垮系统
var serverAutoUnregisterLocker sync.Mutex
func Run() {
ctx := context.Background()
tqtlog.Info(ctx, fmt.Sprintf("auto unregister cron job start exec"))
if serverAutoUnregisterLocker.TryLock() {
defer serverAutoUnregisterLocker.Unlock()
err := a.service.AutoUnregisterCronJob(ctx)
if err != nil {
tqtlog.Error(ctx, fmt.Sprintf(`AutoUnregisterCronJob exec failed, err:%v`, err))
}
} else {
tqtlog.Warn(ctx, fmt.Sprintf("AutoUnregisterCronJob 获取锁失败,存在任务正在执行"))
}
tqtlog.Info(ctx, fmt.Sprintf("auto unregister cron job exec end"))
}
sync.RWMutex读写互斥锁
sync.Mutex
是互斥锁,只有一个信号标量;在 Go 中还有一种读写锁 sync.RWMutex
,对于我们的共享对象,如果可以分离出读和写两个互斥信号的情况,可以考虑使用它来提高读的并发性能。
RWMutex 是基于 Mutex 的,在 Mutex 的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量:
- 读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥,只有在锁释放后才可以继续申请互斥的锁:
- 可以同时申请多个读锁
- 有读锁时申请写锁将阻塞,有写锁时申请读锁将阻塞
- 只要有写锁,后续申请读锁和写锁都将阻塞
有以下几个方法:
func (rw *RWMutex) Lock()
func (rw *RWMutex) RLock()
func (rw *RWMutex) RLocker() Locker
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) Unlock()
Lock()
和 Unlock()
用于申请和释放写锁;RLock()
和 RUnlock()
用于申请和释放读锁,一次 RUnlock()
操作只是对读锁数量减1,即减少一次读锁的引用计数;如果不存在写锁,则 Unlock()
引发 panic,如果不存在读锁,则 RUnlock()
引发 panic;RLocker()
用于返回一个实现了 Lock()
和 Unlock()
方法的 Locker 接口。
由于读锁之间是可以没有阻塞的,所以效率会比较高,如下所示:
func main() {
var (
mux sync.Mutex
state1 = map[string]int{
"a": 65,
}
muxTotal uint64
rw sync.RWMutex
state2 = map[string]int{
"a": 65,
}
rwTotal uint64
)
for i := 0; i < 10; i++ {
go func() {
for {
mux.Lock()
_ = state1["a"]
mux.Unlock()
atomic.AddUint64(&muxTotal, 1)
}
}()
}
for i := 0; i < 10; i++ {
go func() {
for {
rw.RLock()
_ = state2["a"]
rw.RUnlock()
atomic.AddUint64(&rwTotal, 1)
}
}()
}
time.Sleep(time.Second)
fmt.Println("sync.Mutex readOps is", muxTotal)
fmt.Println("sync.RWMutex readOps is", rwTotal)
}
运行代码,可以得到以下结果,明显看到同样对变量进行读的话,读锁的效率比写锁高的多:
sync.Mutex readOps is 485 9713
sync.RWMutex readOps is 359 94874
评论 (0)