1. 锁
Go代码中存在多个goroutine同时操作一个资源的情况,导致结果与预期不符。锁是一种常用的来控制资源的方式,确保同一时刻只有一个goroutine 获取到了资源。
在 go 语言中,锁有两种:互斥锁,读写锁。
互斥锁: sync.Mutex。
读写锁:sync.RWMutex (读写互斥,读读不互斥)。
下面的代码是开启十个goroutine 对一个值进行加操作,每次运行的结果不一定是正确的。
package main
import (
"fmt"
"sync"
)
var sum int
var wg sync.WaitGroup
func cacl() {
defer wg.Done()
for i := 0; i < 50000; i++ {
sum = sum + 1
}
}
func main() {
//开启10个goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go cacl()
}
wg.Wait()
fmt.Printf("sum is %v", sum)
}
加操作不是原子操作,我们为了确保结果的准确需要对加操作加锁。
使用互斥锁修改上面的代码
package main
import (
"fmt"
"sync"
)
var sum int
var wg sync.WaitGroup
//声明锁
var myLock sync.Mutex
func cacl() {
defer wg.Done()
for i := 0; i < 50000; i++ {
//加锁
myLock.Lock()
sum = sum + 1
//释放锁
myLock.Unlock()
}
}
func main() {
//开启10个goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go cacl()
}
wg.Wait()
fmt.Printf("sum is %v", sum)
}
这样每次的结果就是正确的。
读写锁适合读多写少的情景。
下面的代码就模拟了一个读多写少的场景,这样的情况下,使用读写锁消耗的时间比使用互斥锁少的多。
package main
import (
"fmt"
"sync"
"time"
)
//
var wg sync.WaitGroup
//声明读写锁
var rwlock sync.RWMutex
//模拟读数据
func readData() {
defer wg.Done()
//开启读锁
rwlock.RLock()
time.Sleep(time.Millisecond * 2)
//释放读锁
rwlock.RUnlock()
}
//模拟写数据
func writeData() {
defer wg.Done()
//开启写锁
rwlock.Lock()
time.Sleep(time.Millisecond * 10)
//释放写锁
rwlock.Unlock()
}
func main() {
//开始时间
start := time.Now()
//设置1000个读
for i := 0; i < 1000; i++ {
wg.Add(1)
go readData()
}
//设置10个写
for i := 0; i < 10; i++ {
wg.Add(1)
go writeData()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
2. 原子操作
代码中的加锁操作涉及内核态的上下文切换会比较耗时、代价比较高, Go语言提供的原子操作方法它在用户态就可以完成,因此性能比加锁操作更好。
sync/atomic 中可以对几种简单的类型进行原子操作。
这些类型包括 int32、int64、uint32、uint64、uintptr、unsafe.Pointer 。
这些函数的原子操作共有5种:读取操作,写入操作,修改操作,交换操作,比较并交换操作。
使用atomic.Add() 来修改上面的代码
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var sum int32
var wg sync.WaitGroup
func cacl() {
defer wg.Done()
for i := 0; i < 50000; i++ {
atomic.AddInt32(&sum, 1)
}
}
func main() {
//开启10个goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go cacl()
}
wg.Wait()
fmt.Printf("sum is %v", sum)
}
这样也能保证数据的准确。