Giter VIP home page Giter VIP logo

golang_blog's People

Contributors

bikara950225 avatar

Watchers

 avatar

golang_blog's Issues

Golang sync.Mutex学习笔记

一、说明

Golang中以sync.Mutex提供互斥锁的原语,以Lock()方法抢占互斥锁,以Unlock()方法释放互斥锁;Golang的sync.Mutex虽然向外提供了互斥锁的功能,不过底层是以自旋锁+互斥锁的混合方式来实现互斥效果,提供一个具有高吞吐、相对公平的互斥锁。在这里我会根据原文写一下自己的理解~;

二、sync.Mutex结构

官方源码链接:https://github.com/golang/go/blob/master/src/sync/mutex.go#L34

type Mutex struct {
    state int32
    sema  uint32
}
  • state:锁的状态字段,sync.Mutex工作时会以CAS(Compare and Swap)的方式改变它的状态,以实现自旋的效果;它的前3位用于记录当前锁的状态,后29位则是记录被当前锁阻塞的协程的数量,如图所示:
    image
  • sema:当前锁对象的临界值(信号量),当协程自旋取锁循环失败后,就会通过runtime_SemacquireMutex()的系统调用阻塞当前协程,直到锁释放唤醒为止runtime_Semrelease()

三、Lock()方法的工作过程

3.1 Fast Path

官方源码链接:https://github.com/golang/go/blob/master/src/sync/mutex.go#L83

简单地对state进行CAS,如果当前锁的state值为0(表示无任何协程持锁,且无任何协程等待)时,直接获得该锁的所有权,并且标记为mutexLocked状态。当Fast Path取锁失败时,就会进入下面的Slow Path;

if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
    if race.Enabled {
        race.Acquire(unsafe.Pointer(m))
    }
    return
}
... 下面是slow path
lockSlow()

3.2 Slow Path

官方源码链接:https://github.com/golang/go/blob/master/src/sync/mutex.go#L117

当前sync.Mutex对象如果锁被其他协程占有,此时其他协程调用Lock()方法抢锁时就会进入Slow Path部分。Slow Path中,抢锁的方式会根据sync.Mutex当前的状态分为普通模式解饿模式,下面逐一分析。

sync.Mutex的普通模式

正常模式下,新加入锁竞争的协程会尝试多次自旋取锁,如mutex.go#L117-L129
image
关于判断是否还能继续自旋的方法:runtime_canSpin() proc.go
image

自旋取锁失败后, 当前协程就会修改当前sync.Mutex的状态,然后通过runtime_semacquireMutex()函数调用进入阻塞状态;
mutex.go#L132~#152
image

runtime_SemacquireMutex()的调用,调用时会将当前协程加入到sema成员变量的信号量队列对尾中,并且执行gopark()函数让出调度,等待其他携程调用Unlock()时唤醒(当然要遵守FIFO的原则);semaphore可见:https://juejin.cn/post/7133879844114595871;

runtime_SemacquireMutex()被阻塞的协程被唤醒后,协程会将局部变量iter(记录自旋次数的局部变量)置为0,进入下一次的lockSlow()循环,和新加入的协程进行自旋锁的竞争;

https://github.com/golang/go/blob/dev.boringcrypto.go1.18/src/sync/mutex.go#L162
image

总结:正常模式下sync.Mutex提供了自旋锁+互斥锁的混合模式,提供了更加高效的锁竞争处理;不过正常模式下也有缺陷,刚被唤醒的Goroutine因为调度问题(被唤醒后的Goroutine会重新加入到P local runq的队列尾等待调用)不能立即执行,难以与当前抢占自旋锁的Goroutine进行竞争,那么通过runtime_semaquireMutex阻塞的Goroutine很容易会被新加入到锁竞争的Goroutine抢锁,久而久之会产生“饿死”的问题(被阻塞的线程、协程很长一段时间没有抢到锁,阻塞时间过长);为了解决这个问题,于是就有了饥饿模式;

sync.Mutex的饥饿模式

饥饿模式主要是解决因为sema信号量阻塞,长期抢不到锁的协程的“饿死”问题。信号量阻塞的协程被唤醒后,会计算其阻塞的时间,当阻塞时间超出1ms时(见:代码1e6)锁的状态就会变为饥饿状态。在饥饿模式下,被runtime_Semrelease唤醒的Goroutine会直接获得锁并返回,而新加入锁竞争的协程不会进行自旋,直接加入到互斥锁的队尾中等待出队取锁;直到当前唤醒的Goroutine的等待时间小于1ms或者当前唤醒的Goroutine是阻塞队列中的最后一个时,才会退出饥饿模式;见下图的代码:https://github.com/golang/go/blob/dev.boringcrypto.go1.18/src/sync/mutex.go#L165
image

再来看看lockSlow循环的自旋条件,新的Goroutine向饥饿状态的sync.Mutex取锁时,就不会进行自旋取锁了,最终会交给runtime_semacquireMutex()函数,加入到阻塞队列中等待被唤醒:
image

四、Unlock()方法的工作过程

4.1 Fast Path

代码链接:https://github.com/golang/go/blob/dev.boringcrypto.go1.18/src/sync/mutex.go#L210

比较简单的处理,当前sync.Mutex{}.state状态减去mutexLocked(相当于是把mutexLocked这个状态位取消,等价于 m.state &^= mutexLocked)。如果此时state为0,说明该sync.Mutex{}没有其他被阻塞的协程,就会直接退出;否则就会进入unlockSlow()逻辑;

4.2 Slow Path

Unlock的unlockSlow处理方式,和lockSlow一样会区分普通模式以及饥饿模式,不过Unlock的处理比Lock简单得多,所以这里就不分小章节描述了。

普通模式:

image

饥饿模式:

image

饥饿模式下反而不需要对state的阻塞数做调整,因为这一步在Lock()中做了。。。

五、使用sync.Mutex的注意事项

5.1 不要复制sync.Mutex结构体

  1. 结构体复制时会将定义的字段进行值复制,如果对运行中的sync.Mutex结构体进行复制,新的对象的state是基于前者的复制,导致实际运行状态和state不相符,继续使用这种sync.Mutex的话会引发和预期不一样的现象,难以控制;
  2. sync.Mutex复制后,2个锁对象的临界变量就不是同一块内存,不能产生互斥;
func main() {
    lock1 := sync.Mutex{}
    go func(){
        lock1.Lock() // 加锁,state为 0b00000001(这里忽略了高位的24位,都是0),表示当前锁的状态位locked;
    }()
    go func(){
        // 假设这里是在前一个Goroutine后执行
        lock1.Lock() // 加锁,state为 0b00001001(这里忽略了高位的24位,都是0),表示被阻塞得协程为1;
    }()
    time.Sleep(time.Second)
 
    lock2 := lock1
    fmt.Println(lock2) // state为9,明明lock2没有加锁操作,state状态位却为非0,继承了lock1.state;
    lock2.Unlock() // 本来lock2没有被取锁,直接Unlock()调用的话会发生panic,但是这里的执行通过了
 
    ...除此之外还有各种难以预估的异常场景所以不要复制sync.Mutex结构体
}

sync.Mutex复制,导致锁了个寂寞:

type tt struct {
    sync.Mutex
}
func (s tt) method(n int) {
    s.Lock()
    fmt.Println(n)
}
func main() {
    t := tt{}
    t.method(1)
    t.method(2)
}

不过可以借助golang提供的vet工具来进行校验,idea一般也会标记warning来提示:

go vet main.go

Golang 自定义error用法记录

一、说明

总结一下个人在项目中对error的使用理解,以后可以看这个记录来方便记忆;

二、根据比对error内容区分错误类型

最摆烂的方法,通常只有消费方遇到无法通过比对对象、比对类型等方法区分错误时,无可奈何的方法。用法上简单粗暴,但是很蠢,只要生产者稍微改动过错误信息的输出,错误的校验即完全崩溃。demo如下:

// Producer
func Producer() error {
  // 代码路径1
  if err != nil {
    return fmt.Errorf("error 1")
  }
  // 路径2
  if err != nil {
    return fmt.Errorf("error 2")
  }
}
// Consumer
func Consumer() error {
  err := Producer()
  switch err.Error() {
  case "error 1":
    // path1
  case "error2":
    // path2
  }
}

总之是一种对于消费方、生产方而言都是难以维护、迭代的反模式,引以为戒;

三、通过简单的error对象区分错误类型

这种使用场景由自己封装的库提供对应的错误对象,这些错误对象是全局变量,信息固定,是最简单的error定义场景,缺点是无法设置其错误的内容。标准库多数的错误类型都喜欢通过这种方式表现,demo如下:

// Producer
var (
  NotFoundError = errors.New("xxx not found")
  ConnectRefuseError = errors.New("connect refuese")
  InternalError = errors.New("internal error")
  // ... 更多的error对象供外部判断使用
)
// Consumer
// 外部判断错误类型时
func Consumer() error {
  err := Producer()
  switch err {
      case NotFoundError:
          // todo
      case ConnectRefuseError:
         // todo
      case InternalError:
         // todo
  }
}

顺带一提,如果像上面的代码一样直接暴露公共的error对象给外部使用,是会篡改库中描述的错误信息的,但这影响极大,因为我们破坏了一个全局的变量的行为。Golang的话可以把这些公共的错误对象设置为private,库可以提供public方法,以复制error返回的方式让消费者获取error对象,从而提高库中维护的公共对象的安全性(复制的话要小心指针对象和struct对象的eq逻辑~)。

四、定义不同的error实现类型区分错误类型

生产者根据错误类型定义多种error的实现类型,生产者可以通过对error进行interface转换区分错误;对比第2种方法,其好处是生产者可以按需为error实现类型提供可访问的变量供外部修改,加大对错误处理的灵活编辑能力。demo如下:

// Producer
type NotFoundError struct { Message string }
func (s NotFoundError) Error() string { return "not found" }

type InternalError struct { Message string }
func (s InternalError) Error() string { return "internal error" }

// Consumer
func Consumer() {
  err := Producer()
  switch a := err.(type) {
  case  NotFoundError:
    // todo
    // 修改它的内容,带出当前栈调用的信息
    a.Message = "xxxx"
  case InternalErr:
    // todo
  }
}

五、通过wrapErrors包装下游的错误信息,并且通过标准库errors提供的方法区分错误类型

golang在1.13时就提供了wrapErrors给我们包装下游的错误,我们可以通过这个机制同时记录当前栈的错误信息以及保留下游调用返回的错误的类型,并且通过errors.Is或者errors.As方法对抛出的error对象进行回溯,匹配出触发过的错误类型;包装代码很简单,如下:

err := fmt.Errorf("wrapper error: %w, %w", err1, err2)

通过fmt.Errorf()方法以及%w占位符对error进行封装,[1.13, 1.20) 期间只支持单个error wrapper,1.20后支持wrapErrors后就可以一次把多个err对象封装到wrapErrors中了;对wrapErrors进行String输出的结果,与fmt.Errorf("%v", err)一致;区别在于我们可以使用errors.Is()errors.As()对它进行错误类型的区分判断;

5.1 errors.Is

todo

5.2 errors.As

todo

Golang sync.RWMutex学习笔记

一、说明

sync.RWMutex是在sync.Mutex的基础上拓展的一种粒度更细的互斥锁,额外提供了RLock()、RUnlock()方法供使用方对数据读场景的加锁;针对读多写少的场景下,sync.RWMutex会比sync.Mutex有更好的性能;

二、结构

type RWMutex struct{
    w Mutex
    writerSem   uint32
    readerSem   uint32
    readerCount int32
    readerWait  int32
}

字段解析:

  • w:sync.Mutex互斥锁,多个协程写锁竞争时通过它进行互斥;
  • writerSem:写锁的信号量,获取写锁时,如果当前RWMutex被其他协程获取读锁且读锁未释放,当前获得写锁的协程就会通过该临界变量进行阻塞,直到读锁全部释放时才会被唤醒;
  • readerSem:读锁的信号量,获取读锁时,如果当前RWMutex被其他协程获取写锁且写锁未释放,当前获得读锁的协程就会通过该临界变量进行阻塞,直到读锁释放时才会依次唤醒被读阻塞协程;
  • readerCount:当前获得读锁的协程数目,原子变量;在获取读锁时会加一,在获取写锁时会将其减去rwmutexMaxReaders(大小为2的31次幂),所以当readerCount为负值时,说明当前RWMutex已经被获得写锁;
  • readerWait:当前阻塞写锁的读协程数目,原子变量;当RWMutex有读锁未释放时,获得的写锁需要等待到全部读锁释放才能被唤醒,写锁释放时如果RWMutex被其他协程获得写锁,且readerWait减一后为0,则会唤醒被阻塞的写锁的协程;

三、sync.RWMutex主要方法

3.1 Lock()方法

获取写锁的方法,多协程获取写锁时竞争,未获取写锁的协程将会被成员变量w阻塞;当前RWMutex如果被其他协程获取读锁时,获取写锁也会被阻塞,直到所有的读锁都被释放时才能被唤醒,获得写锁;核心代码如图:
image
https://github.com/golang/go/blob/master/src/sync/rwmutex.go#L141

3.2 Unlock()方法

释放写锁的方法,释放后会唤醒写锁占用期间被阻塞的读锁协程;核心代码如图:
image
https://github.com/golang/go/blob/master/src/sync/rwmutex.go#L198

3.3 RLock()方法

获取读锁的方法,多个协程获取读锁是不阻塞的,当RWMutex被获取写锁后,对它获取读锁的话就会被阻塞,直到写锁被释放后才会依次唤醒被阻塞的读锁协程;核心代码如图:
image
https://github.com/golang/go/blob/master/src/sync/rwmutex.go#L64

3.4 RUnlock()方法

释放读锁的方法,释放时会对RWMutex.readerCount减一,如果释放读锁时RWMutx处于写锁被占用的状态(readerCount为负数),则会执行RWMutex{}.rUnlockSlow()方法,进行进一步的处理;

Runlock()部分:

image

rUnlockSlow()部分:

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.