当我们启用多个线程时,如果多个线程都有修改同一个数据时,有可能会造成数据不一致问题,为了避免这个问题可以将修改数据的代码加锁,这样就不会造成数据错乱了。
一、常用的锁与说明
1.1、使用锁时需要导入:sync包
import "sync"
1.2、互斥锁:sync.Mutex()
多个进程时,同一时间只能有一个进程处理加锁的代码(不论读写),当前连接释放后下一个进程才可以处理加锁的代码。
1.3、读写锁:sync.RWMutex()
多个进程同时处理读写操作时,同一时间只可以有一个进程处理写的操作但是可以有多个进程处理读的操作(具体读者数与CPU数相关)。
1.4、读写锁与互斥锁的区别
如果查询的操作较多时,读写锁的处理速度是互斥锁的十倍甚至更多。
二、锁的使用示例
2.1、互斥锁代码示例
package main
import (
"sync"
"fmt"
)
var lock sync.Mutex // 定义一个互斥锁(只有一个能进去,当前连接释放后下一个进程才可以进去)
func testMap() {
a := make(map[int]int, 5)
a[3] = 10
a[1] = 10
a[5] = 10
a[8] = 10
a[10] = 10
for i := 0; i < 2; i++ {
go func(b map[int]int) { // 起了两个线程
lock.Lock() // 写 - 加互斥锁(加锁和解锁之间的代码会上锁)
b[1] = rand.Intn(100) // 如果不加锁,两个进程同时修改时会报错(有一定的几率)
lock.Unlock() // 解锁
}(a)
}
lock.Lock() // 读 - 加锁(读写都需要加锁)
fmt.Println(a) // 结果为:map[1:81 3:10 5:10 8:10 10:10]
lock.Unlock()
}
2.2、读写锁代码示例
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func testRWlock() {
var count int32
a := make(map[int]int, 5)
a[3] = 10
a[1] = 10
a[5] = 10
a[8] = 10
a[10] = 10
for i := 0; i < 2; i++ {
go func(b map[int]int) { // 起了两个线程
rwlock.Lock() // 写 - 加读写锁的写锁(加锁和解锁之间的代码会上锁)
b[1] = rand.Intn(100)
rwlock.Unlock() // 解锁
}(a)
}
for i := 0; i < 30; i++ {
go func(b map[int]int) { // 起了三十个线程
rwlock.RLock() // 读 - 加读写锁的读锁(加锁和解锁之间的代码会上锁)
fmt.Println(a)
atomic.AddInt32(&count, 1) // 原子操作,多个线程同时操作时不会出现数据冲突的的情况(每循环一次count的值+1)
rwlock.RUnlock() // 解锁
}(a)
}
time.Sleep(time.Second * 3)
fmt.Println(atomic.LoadInt32(&count)) // 原子操作读取count变量最新的值,结果:30
}
原子操作说明:
1.atimic(原子操作) 需要导入模块:import ""sync/atomic""
2.原子操作虽然没上锁,但是每个操作都必须一个进程一个进程的操作
3.定义一个变量:var count int32
4.相加操作:atomic.AddInt32(&count,1) 给&count地址的值加1
5.获取地址的值:atomic.LoadInt32(&count)
三、读写锁与互斥锁的效率对比
3.1、互斥锁效率代码
func testLockTime() {
a := make(map[int]int, 5)
var count int32
a[3] = 10
a[1] = 10
a[5] = 10
a[8] = 10
a[10] = 10
for i := 0; i < 2; i++ {
go func(b map[int]int) { // 起了两个goroute
lock.Lock() // 写 - 加互斥锁(加锁和解锁之间的代码会上锁)
b[1] = rand.Intn(100)
time.Sleep(10 * time.Millisecond)
lock.Unlock()
// rwlock.Unlock() // 解锁
}(a)
}
for i := 0; i < 100; i++ {
go func(b map[int]int) { // 起了一百goroute
for {
lock.Lock() // 读 - 加互斥锁的(加锁和解锁之间的代码会上锁)
time.Sleep(time.Millisecond)
atomic.AddInt32(&count, 1) // 原子操作,多个线程同时操作时不会出现数据冲突的的情况(每循环一次count的值+1)
// rwlock.RUnlock() // 解锁
lock.Unlock()
}
}(a)
}
time.Sleep(time.Second * 3)
fmt.Println(atomic.LoadInt32(&count)) // 原子操作读取count变量最新的值,结果为:2172
}
3.2、读写锁效率代码
func testLockTime() {
a := make(map[int]int, 5)
var count int32
a[3] = 10
a[1] = 10
a[5] = 10
a[8] = 10
a[10] = 10
for i := 0; i < 2; i++ {
// 写操作
go func(b map[int]int) { // 起了两个线程
rwlock.Lock() // 写 - 加读写锁的写锁(加锁和解锁之间的代码会上锁)
b[1] = rand.Intn(100)
time.Sleep(10 * time.Millisecond) // 模拟写入操作耗时
rwlock.Unlock() // 解锁
}(a)
}
// 读操作
for i := 0; i < 100; i++ {
go func(b map[int]int) { // 起了一百个线程
for {
rwlock.RLock() // 读 - 加读写锁的读锁(加锁和解锁之间的代码会上锁)
time.Sleep(time.Millisecond) // 读耗时
atomic.AddInt32(&count, 1) // 原子操作,多个线程同时操作时不会出现数据冲突的的情况(每循环一次count的值+1)
rwlock.RUnlock() // 解锁
}
}(a)
}
time.Sleep(time.Second * 3)
fmt.Println(atomic.LoadInt32(&count)) // 原子操作读取count变量最新的值,结果为:227678
}
从上面的两个代码示例里看出:
加了读写锁的代码可以在循环一百次的时间里读取:227678次
加了互斥锁的代码可以在循环一百次的时间里读取:2172次
所以如果查询或者读取的操作多读写锁的效率是互斥锁效率的十倍左右。