go的*锁*包——sync

go的包——sync

上一篇文章介绍了各种锁的基本概念,这篇文章主要学习一下Go的标准库sync包,提供了基本的同步基元.

要注意的是,sync除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些

互斥锁——Mutex

  1. 互斥锁: 同一时刻只能有一个读或写的场景

    var mu sync.Mutex
    
    func main()  {
      mu.Lock()
      // 使用defer释放锁的话会比显示调用Unlock成本高
      defer mu.Unlock()
    }
    
  2. Mutex实现

    // A Mutex is a mutual exclusion lock.
    // The zero value for a Mutex is an unlocked mutex.
    //
    // A Mutex must not be copied after first use.
    type Mutex struct {
    	state int32
    	sema  uint32
    }
    
    • 可以看到Mutex的结构主要是

      • state: 表示锁当前的状态,零值表示未上锁,通过state来进行锁的计数

      • sema: 信号量,实现排队…通过pv操作从等待队列中阻塞/唤醒goroutinue,等待锁的goroutine会挂到等待队列中,并且陷入睡眠不被调度,unlock锁时才唤醒。

    • 详细的图解解析可以参考这两篇文章:

      • 图解Go里面的mutex了解编程语言核心源码实现
      • golang Mutex 实现上的几个巧妙的点
  3. 一个互斥锁只能同时被一个goroutine锁定,其他go程将被阻塞直到互斥锁被解锁,重新争夺互斥锁

  4. 要注意的是: Go中没有重入锁, 对一个已经上锁的Mutex再次上锁会导致程序死锁

读写锁——RWMutes

  1. 读写锁/共享锁: 允许有多个读锁,但只能有一个写锁,读写锁可以分别对读,写进行锁定,一般用于“多读少写”场景

    var rw sync.RWMutex
    
    func main()  {
    	rw.Lock()			// 对写操作进行锁定
    	rw.Unlock()		// 对写操作进行解锁
    	rw.RLock()		// 对读操作进行锁定
    	rw.RUnlock()	// 对读操作进行解锁
    }
    
  2. 写锁权限高于读锁, 写锁会优先锁定,当有写锁时没办法获得读锁, 当只有读锁或无锁时可以获取多个读锁

  3. 源码解析可以看这篇文章:Golang 读写锁RWMutex

单次执行——Once

  1. 使用once可以保证只运行一次,是协程安全的

    var once sync.Once
    func main(){
     	once.Do(
        func(){
        }
    	) 
    }
    
  2. Once的实现分析:

    • Once结构

      // Once is an object that will perform exactly one action.
      type Once struct {
      	// done indicates whether the action has been performed.
      	// It is first in the struct because it is used in the hot path.
      	// The hot path is inlined at every call site.
      	// Placing done first allows more compact instructions on some architectures (amd64/x86),
      	// and fewer instructions (to calculate offset) on other architectures.
      	done uint32
      	m    Mutex
      }
      
      • Done : 计数器,统计执行次数
      • m: 互斥锁
    • Do

      func (o *Once) Do(f func()) {
      // Note: Here is an incorrect implementation of Do:
      //
      //	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
      //		f()
      //	}
      //
      // Do guarantees that when it returns, f has finished.
      // This implementation would not implement that guarantee:
      // given two simultaneous calls, the winner of the cas would
      // call f, and the second would return immediately, without
      // waiting for the first's call to f to complete.
      // This is why the slow path falls back to a mutex, and why
      // the atomic.StoreUint32 must be delayed until after f returns.
      
      if atomic.LoadUint32(&o.done) == 0 {
      	// Outlined slow-path to allow inlining of the fast-path.
      	o.doSlow(f)
      }
      }
      
      func (o *Once) doSlow(f func()) {
      o.m.Lock()
      defer o.m.Unlock()
      if o.done == 0 {
      	defer atomic.StoreUint32(&o.done, 1)
      	f()
      }
      }
      
      • 当done的次数为0的时候执行doSlow
      • 执行后defer将done的值置为1
      • 使用原子操作避免加锁提高了性能
    • Once实现单例模式

      package singleton
      
      import (
      	"fmt"
      	"sync"
      )
      
      // 全局实例
      type singleton struct {
      	data int
      }
      
      var sing *singleton	// 小写私有实例变量
      
      var once sync.Once	// 保证线程安全,只执行一次
      
      func GetInstance(num int) *singleton {
      	once.Do(func() {
      		sing = &singleton{data:num}
      		fmt.Println("实例对象的值为和地址为:", sing, &sing)
      	})
      	return sing
      }
      

等待组——WaitGroup

  1. 用于等待一组协程的结束,可以优雅的避免用sleep或for等待.

    // 1.创建子协程先调用Add增加等待计数
    // 2.子协程结束后调用Done减少协程计数
    // 3.主协程中调用Wait方法进行等待,直到计数器归零继续执行
    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func main() {
    	var wg sync.WaitGroup
    
    	wg.Add(1)
    	go func() {
    		time.Sleep(time.Second * 3)
    		// 子协程结束了等待组就调用done
    		wg.Done()
    		fmt.Println("子协程1结束了")
    	}()
    
    	wg.Wait()
    	fmt.Println("main over")
    }
    
  2. WaitGroup源码分析

条件等待——Cond

  1. 条件等待是不同协程各用一个锁, 互斥锁是不同协程公用一个锁

  2. 条件等待不是像互斥锁那样来保护临界区和共享资源的,是用于协调想要访问共享资源的那些线程,维护一个等待队列,在共享资源的状态发生变化时,可以用来通知被互斥锁所阻塞的线程

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    /**
     * 条件等待
     */
    
    func main() {
    	var wg sync.WaitGroup
    	cond := sync.NewCond(&sync.Mutex{})
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		go func(i int) {
    			cond.L.Lock()
    			defer cond.L.Unlock()
          // Wait()等待通知: 阻塞当前线程,直到收到该条件变量发来的通知
    			cond.Wait()		
    			wg.Done()
          // do other
    			fmt.Println(i)
    		}(i)
    	}
    
    	fmt.Println("正被阻塞。。。")
    
    	time.Sleep(time.Second * 1)
    
    	// Signal()单发通知: 让该条件变量向至少一个正在等待它的通知的线程发送通知,表示共享数据的状态已经改变。
    	cond.Signal()
    
    	fmt.Println("通知已被释放")
    
    	time.Sleep(time.Second * 1)
    
    	fmt.Println("广播")
    
    	// Broadcast广播通知: 让条件变量给正在等待它的通知的所有线程都发送通知。
    	cond.Broadcast()
    
    	wg.Wait()
    
    }
    
    // 正被阻塞。。。
    // 通知已被释放
    // 4
    // 广播
    // 9
    // 3
    // 0
    // 1
    // 2
    // 5
    // 8
    // 7
    // 6
    

协程安全的Map——Sync.Map

  1. 协程安全的map的特性:

    • Sync.Map无须初始化,直接声明即可, 不能使用make创建
    • Sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
    • Sync.Map的键和值以interface{}类型进行保存
    • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false
    • Sync.Map没有获取map数量的方法,可以在Range自行计算
  2. package main
    
    import (
          "fmt"
          "sync"
    )
    
    func main() {
    
        var scene sync.Map
    
        // 将键值对保存到sync.Map
        scene.Store("key", "value")
    
        // 从sync.Map中根据键取值
        fmt.Println(scene.Load("key"))
    
        // 根据键删除对应的键值对
        scene.Delete("key")
    
        // 遍历所有sync.Map中的键值对
        scene.Range(func(k, v interface{}) bool {
            fmt.Println("iterate:", k, v)
            return true
        })
    }
    
  3. Sync.Map源码解析

临时对象池——Pool

Go是自动垃圾回收,在高性能场景下,不能任意产生太多垃圾,会造成gc负担重

解决办法: 使用pool来保存和复用临时对象,减少内存分配和gc压力

  1. Pool是一个可以分别存取的临时对象的集合,是协程安全的,可以缓存申请但未使用的item用于之后的重用来减少GC的压力

  2. Pool不能被复制

    • Get: 从池中选择任意一个item,删除池中的引用计数并提供给调用者,如果没有取得item会返回new的结果
    • Put: 放入池中
    package main
    
    import (
    	"fmt"
    	"runtime"
    	"sync"
    )
    
    /**
     * 临时对象池
     */
    
    func main() {
    
    	pool := sync.Pool{New: func() interface{} {
    		return 0
    	}}
    	pool.Put(1)
    	a := pool.Get()
    	fmt.Println(a)
      pool.Put(1)	
    	runtime.GC()	// gc并不会释放池
    	b := pool.Get()
    	fmt.Println(b)
    }
    
    // 1
    // 1
    
  3. 为什么不叫cache而叫pool, 更多源码的可以参考go夜读的这个视频

你可能感兴趣的:(Golang)