并发概述
在一个线性(即只有一个goroutine)的程序中,程序的执行顺序只由程序的逻辑来决定。在有两个或者更多goroutine的程序中,每一个goroutine内的语句也是按照既定的顺序去执行的,但我们没法知道不同goroutine中的事件的执行顺序,当我们无法确认一个事件是在另一个事件前面或后面发生的话,说明这些事件是并发的。
一个函数在线性的程序中能正常工作,如果在并发情况下,这个函数依然可以正常的工作,我们就说这个函数是并发安全的,并发安全的函数不需要额外的同步工作。我们可以把这个概念概括为一个特定类型的一些方法和操作函数,如果这个类型是并发安全的话,那么所有它的访问方法和操作就都是并发安全的。
sync.Mutex互斥锁
在并发的模型中,我们会用互斥锁来处理并发的问题,锁本身可以认为是一个共享变量,一个线程对锁变量加了互斥锁后,其他线程只能等待,直到锁被释放才能获得锁。在go语言中sync包里的Mutex类型直接支持互斥锁,Lock方法可以获取到token(这里叫锁),并且Unlock方法会释放这个token。
package main
import (
"fmt"
"sync"
"strconv"
)
var balance int
var mu sync.Mutex
/**
* WaitGroup有三个方法:Add(delta int) / Done() / Wait()
* Add 创建goroutine时调用wg.Add(1),也可以在创建多个goroutine前调用wg.Add(n)
* Done 在每个goroutine完成任务后,调用wg.Done(),等同于wg.Add(-1)
* Wait 在等待所有goroutine的地方调用wg.Wait(),它会阻塞主线程,
* 当所有goroutine都调用完wg.Done()之后它会返回
*/
var wg sync.WaitGroup
func Deposit(amount int) {
mu.Lock() //acquire token
balance = balance + amount
fmt.Println("存入金额:" + strconv.Itoa(amount))
mu.Unlock() //release token
wg.Done()
}
func Balance() int {
mu.Lock()
//defer用来执行最后的go语句,一般用于资源释放、关闭连接等操作,会在函数关闭前调用
//多个defer的定义与执行类似于栈的操作:先进后出,最先定义的最后执行。
defer mu.Unlock()
return balance
}
func main() {
wg.Add(3)
//启动3个goroutine,无法判断其执行的先后顺序
//使用了互斥锁避免多协程竞争,该函数是线程安全的
go Deposit(100)
go Deposit(200)
go Deposit(300)
wg.Wait()
fmt.Println(Balance())
}
sync.RWMutex读写锁
package main
import (
"fmt"
"sync"
"strconv"
)
var stock int = 10
var mu sync.Mutex
var wg sync.WaitGroup
//消耗库存
func Destocking() {
if GetStock() > 0 {
mu.Lock()
if stock > 0 {
stock--
fmt.Println("库存剩余:" + strconv.Itoa(stock))
} else {
fmt.Println("1.已没有库存")
}
mu.Unlock()
} else {
fmt.Println("2.已没有库存")
}
wg.Done()
}
//获取当前库存
func GetStock() int {
mu.Lock()
defer mu.Unlock()
return stock
}
func main() {
for i := 0; i < 20; i++ {
wg.Add(1)
go Destocking()
}
wg.Wait()
}
上述程序中,可以看到GetStock函数是获取当前库存,只有读操作并且使用的是互斥锁,当读取的时候会阻塞其它goroutine运行。因为是读取操作,即使多个goroutine同时读取也是安全的,只要在运行的时候没有写操作即可。这时我们需要一种特殊的锁,其允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫做“多读单写”,go语言提供这样的锁是sync.RWMutex。
var mu sync.RWMutex
//获取当前库存
func GetStock() int {
mu.RLock()
defer mu.RUnlock()
return stock
}
这样将GetStock函数换成读锁后,多个goroutine就可以同时读取。
sync.Mutex和sync.RWMutex的区别
go的sync包实现了两种锁Mutex(互斥锁)和RWMutex(读写锁),其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。
Mutex是互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。
RWMutex是读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景。除了提供Lock()和Unlock()外,还提供了RLock()和RUnlock()。RLock()读锁,当有写锁时,无法加读锁,当只有读锁或者没有锁时,可以加读锁,读锁可以加多个,所以适用于"读多写少"的场景。RUnlock()读锁解锁,RUnlock撤销单次RLock调用,它对于其它同时存在的读锁则没有效果。