一、前言
go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁、CAS、原子变量操作类。本节我们先来看看go中读写锁,读写锁相比互斥锁来说并发度更高,在读多写少情况下比较实用。
二、读写锁
在go中可以使用sync.RWMutex获取一个读写锁,读写锁是读写互斥锁,读锁可以被任意多的读goroutine持有,但是写锁只能被某一个goroutine持有。当一个goroutine获取读锁后,其他goroutine也可以获取到读锁,但是如果这时候有goroutine尝试获取写锁,则获取写锁的线程将会被阻塞,这时候如果再有goroutine尝试获取读锁,则其也会被阻塞。
当某个goroutine获取到写锁后,其后尝试获取读锁的goroutine都会被阻塞。
另外读锁是可重入锁,也就是同一个goroutine在可以在持有读锁的情况下再次获取读锁。
在go中获取读锁和写锁方式如下:
var rwLock sync.RWMutex
rwLock.RLock()//获取读锁
rwLock.RUnlock()//释放读锁
rwLock.Lock()//获取写锁
rwLock.Unlock()//释放写锁
三、读写锁例子
3.1 验证多个goroutine可以同时获取读锁
首先我们来验证下多个goroutine可以同时获取读锁:
package main
import (
"fmt"
"sync"
)
var (
wg sync.WaitGroup //信号量
)
var rwlock sync.RWMutex //读写锁
func main() {
wg.Add(1)
//1.
rwlock.RLock()
fmt.Println("--main goroutine get rlock---")
//2.
go func() {
rwlock.RLock()//2.1
fmt.Println("--new goroutine get rlock---")
rwlock.RUnlock()//2.2
wg.Done()//2.3
}()
wg.Wait()//2.4
rwlock.RUnlock()//2.5
}
如上代码首先创建了一个读写锁rwlock,和一个同步用的wg对象
main函数所在goroutine内代码(1)获取到了读锁,然后开启了一个新的goroutine,新goroutine内首先获取读锁,然后在释放,最后让信号量减去1.
main函数所在goroutine在代码2.4等待新goroutine运行结束然后在释放读锁
这里假设同时只有一个goroutine可以获取读锁,则由于main所在goroutine已经在代码1获取到了读锁,所以新goroutine的代码2.1必然被阻塞,所以代码2.3得不到执行,所以main所在gouroutine会一直阻塞到步骤2.4,这两个goroutine就产生了死锁状态。
而实际运行上面代码程序是可正常结束的,这验证了在main所在goroutine获取读锁后释放读锁前,新goroutine也获取到了读锁,也就是多个goroutine可以同时获取读锁。
3.2 读写锁是互斥锁
本节我们来验证当一个goroutine获取到写锁后,其他获取读锁的线程将会被阻塞;当一个goroutine获取到读锁后,另外一个goroutine获取写锁将会被阻塞,如果这时候其他goroutine获取读锁,也会被阻塞
package main
import (
"fmt"
"sync"
)
var (
wg sync.WaitGroup //信号量
)
var rwlock sync.RWMutex //读写锁
func main() {
wg.Add(1)
//1.
rwlock.Lock()
fmt.Println("--main goroutine get wlock---")
//2.
go func() {
rwlock.RLock() //2.1
fmt.Println("--new goroutine get rlock---")
rwlock.RUnlock() //2.2
wg.Done() //2.3
}()
wg.Wait() //2.4
rwlock.Unlock() //2.5
}
如上代码我们修改上面的例子让代码1获取写锁,然后代码2.5释放写锁,新goroutine内还是先获取读锁然后释放读锁。运行上面代码大家猜会输出什么?
首先main所在goroutine获取了写锁,然后执行代码2.4等待新goroutine运行完毕后,释放写锁。
新goroutine则是先尝试获取读锁,由于读写是互斥锁,而现在写锁已经被main所在goroutine持有了,所以新goroutine会阻塞到获取读锁的地方,而main所在goroutine会阻塞到代码2.4,这时候就达到了循环等待的条件,两个goroutine就陷入了死锁状态。运行上面代码会输出:
fatal error: all goroutines are asleep - deadlock!
--main goroutine get wlock---
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x117ffd0)
/usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0x117ffc8)
/usr/local/go/src/sync/waitgroup.go:130 +0x64
main.main()
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:29 +0xd4
goroutine 5 [semacquire]:
sync.runtime_SemacquireMutex(0x118001c, 0x0)
/usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*RWMutex).RLock(0x1180010)
/usr/local/go/src/sync/rwmutex.go:50 +0x4e
main.main.func1()
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:22 +0x31
created by main.main
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:21 +0xc4
上面例子我们验证了本节提的第一个问题,下面看第二个:
package main
import (
"fmt"
"sync"
"time"
)
var (
wg sync.WaitGroup //信号量
)
var rwlock sync.RWMutex //读写锁
func main() {
wg.Add(1)
rwlock.RLock() //1
//2
go func() {
fmt.Println("--new goroutine1 try get wlock---")
rwlock.Lock() //2.1
fmt.Println("--new goroutine1 got wlock---")
rwlock.Unlock() //2.2
fmt.Println("--new goroutine1 release wlock---")
}()
time.Sleep(3 * time.Second) //3
//4
go func() {
rwlock.RLock() //4.1
fmt.Println("--new goroutine2 get rlock---")
rwlock.RUnlock() //4.2
wg.Done() //4.3
}()
wg.Wait() //5
rwlock.RUnlock() //6
}
运行上面代码输出:
--new goroutine1 try get wlock---
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x1180fd0)
/usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0x1180fc8)
/usr/local/go/src/sync/waitgroup.go:130 +0x64
main.main()
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:39 +0x98
goroutine 5 [semacquire]:
sync.runtime_SemacquireMutex(0x1181018, 0x0)
/usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*RWMutex).Lock(0x1181010)
/usr/local/go/src/sync/rwmutex.go:98 +0x74
main.main.func1()
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:22 +0xaa
created by main.main
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:20 +0x62
goroutine 7 [semacquire]:
sync.runtime_SemacquireMutex(0x118101c, 0x0)
/usr/local/go/src/runtime/sema.go:71 +0x3d
sync.(*RWMutex).RLock(0x1181010)
/usr/local/go/src/sync/rwmutex.go:50 +0x4e
main.main.func2()
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:32 +0x31
created by main.main
/Users/luxu.zlx/workspace/learn/go/workspace/lock/src/main/rwlock.go:31 +0x88
代码1main goroutine获取了读锁,然后代码2创建了goroutine1尝试获取写锁,代码3让main goroutine休眠3s以便让goroutine1阻塞到获取写锁后在继续向下运行,然后代码4创建了goroutine2尝试获取读锁,假设获取成功后则会释放读锁然后信号量减去1,然后代码5就会返回,但是这里运行结果出现了死锁说明代码4.1获取读锁阻塞。
3.3 读锁是可重入锁
读锁是可重入锁,同一个gorotine可以多次获取读锁
package main
import (
"fmt"
)
var rwlock sync.RWMutex //读写锁
func main() {
rwlock.RLock() //1
rwlock.RLock() //
fmt.Printf("i got read lock twice")
rwlock.RUnlock() //2
rwlock.RUnlock()
}
如上代码main所在goroutine先两次获取读锁,然后打印输出,然后两次释放读锁,运行上面代码可以正常打印出i got read lock twice说明读锁是可重入锁。
使用下面代码可以验证同一个线程的读锁,不能晋升为写锁:
lock.RLock()
lock.Lock()
fmt.Println("test")
lock.Unlock()
lock.RUnlock()
上面代码执行会报错:同理当一个线程获取到了写锁后尝试获取读锁也会造成deadlock错误
四、总结
本节我们介绍了sync包中的读写锁,读写锁相比互斥锁来说,锁的粒度有所减少,这是因为读写锁,可以让多个读取共享资源的goroutine同时获取读锁,而对互斥锁来说即使是多个读取共享资源的goroutine也只有一个可以获取读锁,其他的都会被阻塞。