互斥锁:
互斥锁是传统并发编程对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型表示.sync.Mutex类型只有两个公开的指针方法Lock和Unlock.前者用于锁定当前的互斥量.后者则用于对当前互斥量进行解锁.
对同一个互斥锁的锁定操作和解锁操作应成对出现.如果锁定了一个已锁定的互斥锁.那么进行重复锁定操作的goroutine将被阻塞.直到该互斥锁回到解锁状态.
示例:
func main() {
var mutex sync.Mutex
fmt.Println("Lock the lock.(main)")
mutex.Lock()
fmt.Println("the lock is locked.(main)")
for i := 1; i <= 3; i++ {
go func(i int) {
fmt.Printf("Lock the lock.(g%d)\n", i)
mutex.Lock()
fmt.Printf("the lock is locked. (g%d)\n", i)
}(i)
}
time.Sleep(time.Second)
fmt.Println("Unlock the lock.(main)")
mutex.Unlock()
fmt.Println("the lock is unlocked.(main)")
time.Sleep(time.Second)
}
执行main函数的goroutine简称为main.该函数中又启了三个goroutine.分别命名g1 g2 g3.启动三个goroutine之前.main已经对互斥锁进行锁定了.然后3个函数开始对mutex进行锁操作.
执行结果:
从上面的结果可以看出.main抢到锁进行睡眠的时候.g1 g2 g3都进行锁操作然后进入了阻塞.当main睡眠结束.释放锁之后.g3抢到了锁.
对于一个未锁定的互斥锁进行解锁操作时.就会引发一个运行时恐慌.避免这种情况最简单的方式依然是使用defer语句.这样更容易保证解锁操作的唯一性.
示例:
func main() {
var mutex sync.Mutex
defer func() {
fmt.Println("Try to recover the panic")
if err := recover(); err != nil {
fmt.Printf("Recovered the panic(%#v).\n", err)
}
}()
fmt.Println("Lock the lock.(main)")
mutex.Lock()
fmt.Println("the lock is locked.(main)")
fmt.Println("Unlock the lock.(main)")
mutex.Unlock()
fmt.Println("the lock is unlocked.(main)")
fmt.Println("Unlock the lock again.(main)")
mutex.Unlock()
}
执行结果:
读写锁:
读写锁即针对读写操作的互斥锁.它与普通的互斥锁最大的不同.就是可以分别针对读操作和写操作进行锁定和解锁操作.读写遵循的访问控制规则与互斥锁有所不同.读写锁控制下的多个写操作之间都是互斥的.并且写操作与读操作之间也是互斥的.多个读操作之间却不存在互斥关系.在这样的互斥策略之下.读写锁可以在大大降低因使用锁而造成的性能损耗的情况.完后对共享资源的访问控制.
读锁加锁:
func (rw *RWMutex) RLock() {
...
}
读锁解锁:
func (rw *RWMutex) RUnlock() {
...
}
写锁加锁:
func (rw *RWMutex) Lock() {
...
}
写锁解锁:
func (rw *RWMutex) Unlock() {
...
}
写解锁会试图唤醒所有因欲进行读锁定而被阻塞的goroutine.而读解锁只会在已无任何读锁定的情况下.试图唤醒一个因欲进行写锁定而被阻塞goroutine.若对一个未被写锁定的读写锁进行解锁.就会引发一个不可恢复的运行时恐慌.而对一个未被读锁定的读写锁进行读解锁同样也会如此.
示例:
func main() {
var rwm sync.RWMutex
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Printf("Try to lock for reading...[%d]\n]", i)
rwm.RLock()
fmt.Println("Locked for reading.[%d]\n", i)
time.Sleep(2 * time.Second)
fmt.Printf("Try to unlock for reading...[%d]\n]", i)
rwm.RUnlock()
fmt.Printf("Unlocked for reading...[%d]\n", i)
}(i)
}
time.Sleep(time.Millisecond * 100)
fmt.Println("Try to lock for writing...")
rwm.Lock()
fmt.Println("Locked for writing...")
}
执行结果:
从上面的内容可以看出Locked for writing 总是出现在最后一行.可以体会出读锁是可以重复获取的.而读写就是互斥的.
条件变量:
Go标准库中的sync.Cond类型代表了条件变量.与互斥锁和读写锁不同.简单的声明无法创建出一个可用的条件变量.这需要用到sync.NewCond函数.函数声明如下:
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
条件变量总是要与互斥量组合使用.sync.NewCond函数的唯一参数是sync.Locker类型的.具体参数值可以是一个读写锁也可以是一个互斥锁.sync.NewCond被调用后.会返回一个*sync.Cond类型的值,可以调用这个值来操作条件变量.
*sync.Cond类型的有三个方法.Wait Signal Broadcast.fan别代表了等待通知 单发通知和广播通知的操作.
wait方法会自动的对与该条件变量关联的那个锁进行解锁.并且使它所在的goroutine阻塞.一旦接收到通知.该方法所在的goroutine就会被唤醒.并且该方法会立即尝试锁定该锁.方法Signal和Broadcast的作用都是发送通知.以唤醒正在为此阻塞的goroutine.不同的是.前者的目标只有一个.后者的目标是所有.
原子操作:
原子操作即执行过程中不可被中断的操作.在针对某个值的原子操作执行过程当中.CPU绝不会再去执行其它针对该值的操作.无论这些操作是否为原子操作.
Go语言提供的原子操作都是非侵入式的.它们由标准库代码包sync/atomic中的众多函数代表.可以通过调用这些函数对几种简单类型执行原子操作.
增或减:
用于增或减的原子操作(以下简称原子增或减操作)的函数名称都以Add为前缀.后跟针对具体类型的名称.
示例:
func main() {
var a int32 = 1
var b int64 = 1
int32Value := atomic.AddInt32(&a, 1)
int64Value := atomic.AddInt64(&b, 1)
fmt.Println(int32Value)
fmt.Println(int64Value)
reduceInt32 := atomic.AddInt32(&a, -1)
reduceInt64 := atomic.AddInt64(&b, -1)
fmt.Println(reduceInt32)
fmt.Println(reduceInt64)
}
执行结果:
比较并交换:
比较并交换即"Compare And Swap"简称CAS.在sync/atomic包中.这类原子操作由名称以"CompareAndSwap"为前缀的若干函数代表.
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
函数接受三个参数.第一个参数的值是指向被操作的指针值.该值的类型是*int32.后两个参数的类型也是32.分别代表被操作的旧值和新值.函数在被调用后.会先判断参数addr的指向和旧值old的值是否相等.当判断成功后.才会用新值替换旧值.否则.后面的替换就会被忽略.
与前面的锁相比.CAS操作有明显不同.它总是假设被操作值未曾改变(即与旧值相等).并一旦确认这个假设的真实性就立即进行值替换.使用锁则是更加谨慎的做法.总是先假设会有并发操作会修改被操作值.并需要使用锁将相关操作放入临界区加以保护.可以说.使用锁的做法趋于悲观.而CAS操作的做法比较乐观.
CAS操作的优势是.可以在不创建互斥量和不形成临界区的情况下.完成并发安全的值替换操作.可以大大减少同步对程序性能的损耗.当然,CAS也有劣势.是被操作值被频繁变更的情况下.CAS操作并不那么容易成功.有时候.可能不得不利用for循环来进行多次尝试.
示例:
func main() {
var value int32
for i := 0; i < 10; i++ {
go func() {
for {
v := value
if atomic.CompareAndSwapInt32(&v, 0, 1) {
break
}
}
}()
}
}
从上面例子可以看出.只有保证CAS操作成功后.for循环才会结束退出循环.CAS操作虽然不会阻塞goroutine.但是会不断自旋造成性能上的消耗.
载入:
前面展示的for循环中.使用语句v:=value为变量v赋值.要注意.在读取value的过程中.并不能保证没有对此值的并发读写操作.为了原子的读取某个值.sync/atomic代码包也提供了一系列函数.这些函数的名称都以"load"(意为载入)为前缀.
示例:
func main() {
var value int32
for i := 0; i < 10; i++ {
go func() {
for {
v := atomic.LoadInt32(&value)
if atomic.CompareAndSwapInt32(&v, 0, 1) {
break
}
}
}()
}
}
函数atomic.LoadInt32接受一个*int32类型的指针值.并会返回该指针值指向的那个值.
注意:虽然这里使用了atomic.LoadInt32函数原子的载入value的值.后面的CAS操作仍然是有必要的.因为.赋值语句后面的if语句并不会原子执行.在它们执行期间.CPU仍然可能执行其它针对Value的操作.
存储:
与读操作相对应的是写入操作.而sync/atomic包也提供了对应的存储函数.这些函数的名称均以"Store"为前缀.
在原子的存储某个值的过程中.任何CPU都不会进行针对同一个值的读写操作.如果把所有针对此值的写操作都改为原子操作.就绝不会出现针对此值的读操作因被并发的进行.而读到修改了一半的值的情况.
原子的值存储操作总会成功.因为它并不关系操作的旧值是什么.这与前面的CAS操作有明显区别.
函数atomic.StoreInt32会接受两个参数.第一个参数的类型是*int32.它同样是指向被操作值的指针值.第二个参数是int32类型.它的值是欲存储的新值.
交换:
在sync/atomic代码包中还有一些函数.它们的功能与前文所讲的CAS操作和原子载入操作都有相似之处.这里的功能可以称为原子交换操作.这类函数的名称都以"Swap"为前缀.
与CAS操作不同.原子操作不会关心被操作的旧值.而是直接设置新值.但它又比原子存储操作多做了一步.它会返回被操作的旧值.此类操作比CAS操作的约束更少.同时又比原子载入操作的功能更强.
func SwapInt32(addr *int32, new int32) (old int32)
从函数可知.它接受两个参数.其中第一个参数代表了被操作的内存地址的*int32类型值.第二个参数用来表示新值.注意.该函数是有结果值的.该值即被新值替换掉的旧值.函数被调用后.会把第二个参数的值置于第一个参数值所代表的的内存地址上.并将之前在该地址上的那个值作为结果返回.
白衣裳朱阑立.凉月趖西.点鬓霜微.岁晏知君归不归.
残更目断传书雁.尺素还稀.一味相思.准拟想看似旧时.
语雀地址www.yuque.com/itbosunmian…?
《Go.》 密码:xbkk 欢迎大家访问.提意见.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路