Go并发编程 - (四)同步、锁

Posted by YaPi on August 28, 2019

sync.Mutex
读写锁 sync.RWmutex

读写锁包含了四种基本方法

  • func(*RWMutex) Lock()
  • func(*RWMutex) UnLock()
  • func(*RWMutex) RLock()
  • func(*RWMutex) RUnLock()

读锁并不影响其他的读,但是会阻塞对应的写锁。写解锁会试图唤醒所有因欲进行读锁定而被阻塞的goroutine。而读解锁只会在已无任何读锁定的情况下,试图唤醒一个因欲进行写锁定而被阻塞的goroutine。若对未进行写锁定的读写锁进行解锁,会panic,读锁也一样。

package main

import (
	"fmt"
	"sync"
	"time"
)

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.Printf("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.")
}

输出
Try to lock for reading 0
Locked for reading. 0
Try to lock for reading 1
Locked for reading. 1
Try to lock for reading 2
Locked for reading. 2
Try to lock for writing...
Try to unlock for reading 1
Unlocked for reading [1]
Try to unlock for reading 0
Try to unlock for reading 2
Unlocked for reading [0]
Unlocked for reading [2]
Locked for writing.

当所有加的读锁都解锁了过后,对写锁进行加锁的操作才会执行
条件变量

Go标准库中的sync.Cond类型代表了条件变量。无法直接声明,需要用方法创建

func NewCond(l Locker)*Cond

需要传入Locker的实现。比如一个互斥锁或者一个读写锁。

*sync.Cond类型的方法集合中有三个方法

  • Wait 等待通知
  • Signal 单发通知
  • Broadcast 广播通知

Wait方法会自动对与该条件变量关联的那个锁进行解锁(这点很重要)。并且使它所在的goroutine阻塞。一旦几首到通知,该方法所在的goroutine就会被唤醒,并且该方法会立即尝试锁定该锁。方法Signal和Broadcast的作用都是发送通知,以唤醒正在为此阻塞的goroutine。

recond := sync.NewCond(sync.Mutex)

注意点

  • 一定要在调用rcond的Wait方法之前锁定与之关联的读锁。
  • 一定不要忘记在读取数据块之后及时解锁与条件变量rcond关联的那个读锁,否则对读写锁的写锁定操作将会阻塞相关的goroutine。其根本原因是,条件变量rcond的Wait方法在返回之前会重新锁定与之关联的那个读锁。
原子操作
  • atomic.LoadInt32(&value) 原子的读取某个数据
sync/atomic.Value

sync/atomic.Value是一个结构体类型。它用于存储需要原子读写的值。与sync/atomic包中其他函数不同,它可以接收被操作值的类型不限。

声明

var atomicVal atomic.Value

该类型有两个公开的指针方法–Load和store。前者用于原子地读取原子值实例中存储的值。会返回一个interface{}类型的结果且不接收任何参数。后者用于原子地在原子值实例中存储一个值。

对于store方法的限制

  • 作为参数传入该方法的值不能为nil.
  • 作为参数传入该方法的值必须与之前传入的值(如果有的话)类型相同。也就是说,一旦原子值实例存储了某一个类型的值,那么它之后存储的值就都必须是该类型的。
package main

import (
	"fmt"
	"sync/atomic"
)

func main()  {
	var countVal atomic.Value
	countVal.Store([]int{1,3,5,7})
	anotherStore(&countVal)
	fmt.Printf("The count value: %v",countVal.Load())
}

func anotherStore(countVal *atomic.Value)  {
	countVal.Store([]int{2,4,6,8})
}

// The count value: [2 4 6 8]

若不是指针类型会输出 [1,3,5,7]
sync.Once 执行一次

sync.Once也是开箱即用的

var once sync.Once
once.Do(func(){fmt.Println("Onece!")})

对同一个sync.Once类型值的Do方法的有效调用次数永远会是1。也就是说,无论调用这个方法多少次,也无论在多次调用时传递给它的参数值是否相同,都仅有第一次调用是有效的。

// 源码
func (o *Once) Do(f func()) {
    // 获取是否运行标记
	if atomic.LoadUint32(&o.done) == 1 {
		return
	}
	// Slow-path.
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}
sync.WaitGroup

sync.WaitGroup类型的值是并发安全的,也是开箱即用的。该类型有3个指针方法,即:Add、Done、Wait(类似java的countDownlunch)

当调用sync.WaitGroup类型值的Wait方法时,它回去检查给定计数。如果该计数为0,那么该方法会立即返回,且不会对程序的运行产生影响。但是,如果这个计数大于0,该方法调用的那个goroutine就会阻塞,同时,等待计数会加1.直到该值的Add或Done方法被调用时,发现给定计数变回0,该值才会去唤醒因此而阻塞的所有goroutine.同时清零等待计数。

sync.WaitGroup使用规则

  • 对一个sync.WaitGroup类型值的Add方法的第一次调用,发生在Done和Wait之前
  • Add过后的值不能是负数,否则会报错