在Go
语言中,信道的地位非常高,它是first class
级别的,面对并发问题,我们始终应该优先考虑使用信道,如果通过信道解决不了的,不得不使用共享内存来实现并发编程的,那Golang
中的锁机制,就是你绕不过的知识点了。
当多个goroutine
同时操作一个资源的时候,可能会造成资源的竞争。下面通过一段代码来展示这个问题:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var x int
func main() {
// 开启两个协程来对全局变量x进行操作
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
increase(&x)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10000; i++ {
increase(&x)
}
}()
wg.Wait()
fmt.Println(x)
}
func increase(n *int) {
*n = *n + 1
}
当多次执行上述代码,得到的结果每次都大相径庭。12886
,12258
, 11514
...
在上面的示例代码片中,我们开启了两个goroutine
分别执行increase
函数,这两个goroutine
在访问和修改全局的x变量时就会存在数据竞争,某个goroutine
中对全局变量x
的修改可能会覆盖掉另一个goroutine
中的操作,所以导致最后的结果与预期不符。
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个goroutine
可以访问共享资源。Go语言中使用sync
包中提供的Mutex
类型来实现互斥锁。
sync/Mutex
提供两个方法:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var mutex sync.Mutex
var x int
func main() {
wg.Add(2)
go func() { // 多个协程对同一个全局变量进行更改
defer wg.Done()
for i := 0; i < 10000; i++ {
mutex.Lock()
increase(&x)
mutex.Unlock()
}
}()
go func() { // 多个协程对同一个全局变量进行更改
defer wg.Done()
for i := 0; i < 10000; i++ {
mutex.Lock()
increase(&x)
mutex.Unlock()
}
}()
// 问题: 会造成资源竞争,导致得不到想要的数据
// 解决方法:给要操作的变量进行加锁操作,但这会导致程序编程单协程,因为加锁时会阻塞,其他协程也无法运行。
wg.Wait()
fmt.Println(x)
}
func increase(n *int) {
*n = *n + 1
}
将上面的代码编译后多次执行,每一次都会得到预期中的结果——20000。
互斥锁会保证每个同一时间有且只有一个goroutine
可以获取到资源,其他goroutine
会处于阻塞状态,等待那个获取到资源重新释放后,才会重新竞争获取锁。
使用Mutex
锁虽然很简单,但仍然有几点需要注意:
defer
语句Mutex
是最简单的一种锁类型,他提供了一个傻瓜式的操作,加锁解锁加锁解锁,让你不需要再考虑其他的。 简单同时意味着在某些特殊情况下有可能会造成时间上的浪费,导致程序性能低下。
很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync
包中的RWMutex
类型。
sync/RWMutex
提供五个方法:
Locker
接口的读写锁RWMutex
为了保证数据的安全,它规定了当有人还在读取数据(即读锁占用)时,不允计有人更新这个数据(即写锁会阻塞);为了保证程序的效率,多个人(线程)读取数据(拥有读锁)时,互不影响不会造成阻塞,它不会像 Mutex 那样只允许有一个人(线程)读取同一个数据。
下面我们使用代码构造一个读多写少的场景,然后分别使用互斥锁和读写锁查看它们的性能差异。
var (
x int64
wg sync.WaitGroup
mutex sync.Mutex
rwMutex sync.RWMutex
)
// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
mutex.Lock() // 加互斥锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
mutex.Unlock() // 解互斥锁
wg.Done()
}
// readWithLock 使用互斥锁的读操作
func readWithLock() {
mutex.Lock() // 加互斥锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
mutex.Unlock() // 释放互斥锁
wg.Done()
}
// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
rwMutex.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwMutex.Unlock() // 释放写锁
wg.Done()
}
// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
rwMutex.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwMutex.RUnlock() // 释放读锁
wg.Done()
}
func do(wf, rf func(), wc, rc int) {
start := time.Now()
// wc个并发写操作
for i := 0; i < wc; i++ {
wg.Add(1)
go wf()
}
// rc个并发读操作
for i := 0; i < rc; i++ {
wg.Add(1)
go rf()
}
wg.Wait()
cost := time.Since(start)
fmt.Printf("x:%v cost:%v\n", x, cost)
}
我们假设每一次读操作都会耗时1ms,而每一次写操作会耗时10ms,我们分别测试使用互斥锁和读写互斥锁执行10次并发写和1000次并发读的耗时数据。
// 使用互斥锁,10并发写,1000并发读
do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s
// 使用读写互斥锁,10并发写,1000并发读
do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms
从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。
基于Nginx+Supervisord+uWSGI+Django1.11.1+Python3.6.5构建