golang同步总结

目录

 

条件变量

原子操作

只会执行一次

WaitGroup

context.Context

临时对象池

参考资料


1.互斥锁
表示:sync.Mutex,类型sync.Mutex的零值表示了未被锁定的互斥量
作用:保证在同一时刻仅有一个线程访问共享数据。
规则:1)当对一个已处于解锁状态的互斥锁进行解锁操作的时候,就会引发一个运行时恐慌;2)当对一个已处于锁定状态的互斥锁进行锁定操作时,就会被阻塞;3)对于同一个互斥锁的锁定操作和解锁操作总是应该成对地出现,一般会在锁定互斥锁之后紧接着用defer语句保证该互斥锁的及时解锁;4)互斥锁可以被直接的在多个Goroutine之间共享但是还是建议把对同个互斥锁的锁定和解锁操作放在同一个层次的代码块中,应该使代表互斥锁的变量的访问权限尽量低,避免在不相关的流程中被误用,导致程序不正确的行为。
方法:Lock和Unlock,表示对互斥量的锁定和解锁。
示例:
func Lock1(){
    var lock sync.Mutex
    fmt.Println("main>lock is locking")
    lock.Lock()
    fmt.Println("main>lock is locked")
    go func(){
        for i:=0;i<3;i++{
            fmt.Println("go routine>lock is locking",i)
            lock.Lock()
            fmt.Println("go routine>lock is locked",i)
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("main>lock is unlocking")
    lock.Unlock()
    fmt.Println("main>lock is unlocked")
    time.Sleep(5*time.Second)
}
运行结果:
=== RUN   TestLock1
main>lock is locking
main>lock is locked
go routine>lock is locking 0
main>lock is unlocking
main>lock is unlocked
go routine>lock is locked 0
go routine>lock is locking 1
--- PASS: TestLock1 (6.00s)
PASS

2.读写锁
表示:sync.RWMutex,其零值已经是立即可用的读写锁了,可分别针对读操作和写操作进行锁定和解锁操作。
规则:1)写解锁在进行的时候会试图唤醒所有因欲进行读锁定而被阻塞的Goroutine,读解锁在进行的时候会在已无任何读锁定的情况下试图唤醒一个因欲进行写锁定而被阻塞的Goroutine。2)对一个未被写锁定的的读写锁进行写解锁,会引发恐慌;而对一个未被读锁定的读写锁进行读解锁则不会。3)对于同一个读写锁来说,施加在它之上的读锁定可以有多个。简单说,读写锁控制下的多个写操作之间是互斥的;写操作和读操作是互斥的;多个读操作之间不存在互斥关系。
方法:Lock,Unlock,RLock,RUnlock,RLocker,分别表示写锁定,写解锁,读锁定,读解锁,最后一个方法会返回一个实现了sync.Locker接口的值,这个结果的Lock和Unlock方法分别针对该读写锁的读锁定和读解锁操作。其意义在于可以在以后以相同的方式对该读写锁中的写锁和读锁进行操作。
示例:
对同一个文件进行操作:写操作之间不能彼此干扰、读操作按顺序独立的操作。
func (df *myDataFile) Read() (d Data, err error) {
    // 读取并更新读偏移量offset
    ...

    //读取一个数据块,fmutex为读写锁
    bytes := make([]byte, df.dataLen)
    for {
        df.fmutex.RLock()  //读锁定
        _, err = df.f.ReadAt(bytes, offset)
        if err != nil {
            if err == io.EOF {
                df.fmutex.RUnlock() //读解锁
                continue
            }
            df.fmutex.RUnlock() //读解锁
            return
        }
        d = bytes
        df.fmutex.RUnlock() //读解锁
        return
    }
}

func (df *myDataFile) Write(d Data) (err error) {
    ...

    //写入一个数据块
    df.fmutex.Lock() //写锁定
    defer df.fmutex.Unlock() //写解锁
    _, err = df.f.Write(d)
    return
}

条件变量

表示:sync.Cond,区别于互斥锁,简单的声明无法创建出一个可用的条件变量,需要使用sync.NewCond。条件变量需要与互斥量结合使用
作用:共享数据的状态发生变化时,通知其他因此而被阻塞的线程。
方法:Wait、Signal、Broadcast,分别表示等待通知、单发通知、广播通知。后两者作用是发送通知以唤醒正在为此被阻塞的Goroutine。前者会自动地对与该条件变量关联的那个锁进行解锁,并使调用方所在的Goroutine被阻塞。
Wait:阻塞当前线程,直至收到该条件变量发来的通知。
Signal:让该条件变量向至少一个正在等待它的通知的线程发送通知,以表示某个共享数据的状态已经改变。
Broadcast:让条件变量给正在等待它的通知的所有线程都发送通知,以表示某个共享数据的状态已经改变。
signal和broadcast方法调用之前无需锁定与之关联的锁,而wait需要。
示例:
df.rcond = sync.NewCond(df.fmutex.RLocker())
func (df *myDataFile) Read() (d Data, err error) {
    // 读取并更新读偏移量
    ...

    //读取一个数据块,fmutex为读写锁,rcond为读操作需要用到的条件变量
    bytes := make([]byte, df.dataLen)
    df.fmutex.RLock() //读锁定
    defer df.fmutex.RUnlock() //读解锁
    for {
        _, err = df.f.ReadAt(bytes, offset)
        if err != nil {
            if err == io.EOF {
                df.rcond.Wait() //条件变量等待通知
                continue
            }
            return
        }
        d = bytes
        return
    }
}

func (df *myDataFile) Write(d Data) (err error) {
    ...

    //写入一个数据块
    df.fmutex.Lock() //写锁定
    defer df.fmutex.Unlock() //写解锁
    _, err = df.f.Write(d) 
    df.rcond.Signal() //条件变量单发通知
    return
}

原子操作

相关包:sync/atomic,原子操作即进行过程中不能被中断的操作。
类型包括:int32,int64,uint32,uint64,uintptr,unsafe.Pointer
操作:增或减add、比较并交换compare and swap,CAS、载入load、存储store、交换swap
>增或减
atomic.AddInt32(&a,3)
atomic.AddInt64(&a,-3),需注意uint32,uint64减不是这么运算的,其运算如下:
atomic.AddUint32(&b,^uint32(-NN-1)),其中NN表示一个负整数。
>比较并交换
CompareAndSwapInt32(addr &int32,old,new int32)(swapped bool)
并发安全地更新一些类型的值,优先选择CAS(比较并交换)。比较并交换操作即 CAS 操作,是有条件的交换操作,只有在条件满足的情况下才会进行值的交换。所谓的交换指的是,把新值赋给变量,并返回变量的旧值。在进行 CAS 操作的时候,函数会先判断被操作变量的当前值,是否与我们预期的旧值相等。如果相等,它就把新值赋给该变量,并返回true以表明交换操作已进行;否则就忽略交换操作,并返回false。可以看到,CAS 操作并不是单一的操作,而是一种操作组合。这与其他的原子操作都不同。正因为如此,它的用途要更广泛一些。例如,我们将它与for语句联用就可以实现一种简易的自旋锁(spinlock)。
>载入
v:=atomic.LoadInt32(&value)原子地读取变量value的值,当前计算机中的任何CPU都不会进行其他针对此值的读或写操作
>存储
StoreInt32
原子地存储某个值的过程中,任何CPU都不会进行针对同一个值的读或写操作。
>交换
SwapInt32
直接设置新值,返回旧值

一旦我们确定了在某个场景下可以使用原子操作函数,比如:只涉及并发地读写单一的整数类型值,或者多个互不相关的整数类型值,那就不要再考虑互斥锁了。原子操作一定程度可以替换锁。原子操作由底层硬件支持,锁由操作系统提供的API实现。
示例:
func (df *myDataFile) Read() (rsn int64, d Data, err error) {
    // 读取并更新读偏移量,roffset表示读操作需要用到的偏移量
    var offset int64
    for {
        offset = atomic.LoadInt64(&df.roffset) //原子地读取roffset
        //这里进行了CAS操作,依次传入三个值:被操作值的地址、被操作数的旧值、欲设置的新值
        if atomic.CompareAndSwapInt64(&df.roffset, offset, (offset + int64(df.dataLen))) {
            break
        }
    }

    //读取一个数据块
    ...
}

只会执行一次

表示:sync.Once
特点:once.Do:无论我们在多次调用时传递给它的参数是否相同,都仅有第一次调用是有效的。
应用场景:执行仅需执行一次的任务。比如数据库连接池的初始化任务;一些需持续运动的实时监测任务。
连接数据库的代码其实不太适合放到 Do 里面执行,或者说不太恰当。初始化数据库链接的代码可以放到里面。而断链重连的机制也应该在其中。
方法:Once类型的Do方法只接受一个参数,这个参数的类型必须是func(),即:无参数声明和结果声明的函数。Once类型中还有一个名叫done的uint32类型的字段。它的作用是记录其所属值的Do方法被调用的次数。不过,该字段的值只可能是0或者1。一旦Do方法的首次调用完成,它的值就会从0变为1。
示例:
func OnceDo(){
    var num int
    sign:=make(chan bool)
    var once sync.Once
    f:=func(ii int)func(){
        return func(){
            num=(num+ii*2)
            sign<-true
        }
    }

    for i:=0;i<3;i++{
        fi:=f(i+1)
        go once.Do(fi)
    }

    for j:=0;j<3;j++{
        select{
        case <-sign:
            fmt.Println("received a signal!")
        case <-time.After(time.Second):
            fmt.Println("time out!")
        }
    }
    fmt.Println("num=",num)
}

运行结果:
received a signal!
time out!
time out!
num= 2

WaitGroup

表示:sync.WaitGroup,并发安全的,可以对多个Goroutine的运行进行简单的协调。它比通道更加适合实现这种一对多的 goroutine 协作流程。
规则:Add方法的调用应该在Done方法之前或Wait方法之前。
方法:Add,Done,Wait。分别表示增大或减少其中的计数值、计数值减一、检查该值中的计数值。其中Wait方法会判断如果计数值为0,则立即返回,不会对程序的运行产生任何影响;如果计数值>0,那么该方法的调用方所属的那个Goroutine就会被阻塞,直到该计数值重新变为0,为此而阻塞的所有GoRoutine才会被唤醒。
示例:
func WaitGroupOp(){
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        fmt.Println("go routine")
        wg.Done()
    }()
    wg.Wait()
}

context.Context

定义:上下文,或者说goroutine的上下文。Context是线程安全的,可以放心的在多个goroutine中传递。
场景:不能在一开始就确定执行子任务的 goroutine 的数量;有很多goroutine都需要控制结束;goroutine又衍生了其他更多的goroutine、一层层的无穷尽的goroutine等goroutine较复杂的关系链。
方法:Deadline、Done、Err、Value。
Deadline:获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Done:返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。这个接收通道的用途并不是传递元素值,而是让调用方去感知“撤销”当前Context值的那个信号。
Err:返回取消的错误原因,因为什么Context被取消。
Value:获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。在我们调用含数据的Context值的Value方法时,它会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其父值中继续查找。如果其父值中仍然未存储相等的键,那么该方法就会沿着上下文根节点的方向一路查找下去。注意,Context接口并没有提供改变数据的方法。因此,在通常情况下,我们只能通过在上下文树中添加含数据的Context值来存储新的数据,或者通过撤销此种值的父值丢弃掉相应的数据。如果你存储在这里的数据可以从外部改变,那么必须自行保证安全。
理解:所有的Context值共同构成了一颗代表了上下文全貌的树形结构。这棵树的树根(或者称上下文根节点)是一个已经在context包中预定义好的Context值,它是全局唯一的。通过调用context.Background函数,我们就可以获取到它。这里注意一下,这个上下文根节点仅仅是一个最基本的支点,它不提供任何额外的功能。也就是说,它既不可以被撤销(cancel),也不能携带任何数据。但是context包中包含了四个用于繁衍Context值的函数,即:WithCancel、WithDeadline、WithTimeout和WithValue。这些函数的第一个参数的类型都是context.Context,而名称都为parent,表示将会产生的Context值的父值。
示例:
/*
context.Background() 返回一个空的Context,这个空的Context一般用于整个Context树的根节点。然后我们使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。
示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。
 */
func ContextOp() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx,"【监控1】")
    go watch(ctx,"【监控2】")
    go watch(ctx,"【监控3】")

    time.Sleep(1 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name,ctx.Err(),"监控退出,停止了...")
            return
        default:
            fmt.Println(name,"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}
func ContextOp1(){
    ctx,cancel:=context.WithCancel(context.Background())
    go watch1(context.WithValue(ctx,"key","监控1"))
    go watch1(context.WithValue(ctx,"key","监控2"))
    go watch1(context.WithValue(ctx,"key","监控3"))

    time.Sleep(1 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    time.Sleep(5 * time.Second)
}
func watch1(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Value("key"),ctx.Err(),"监控退出,停止了...")
            return
        default:
            fmt.Println(ctx.Value("key"),"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}

临时对象池

表示:sync.Pool,非开箱即用。sync.Pool的New字段代表着创建临时对象的函数,其类型是没有参数但有唯一结果的函数类型,即:func() interface{}。存放可被重复使用的值的容器,此类容器是自动伸缩的,高效的,同时也是并发安全的,它们的创建和销毁可以在任何时候发生,并且完全不影响程序的性能。
方法:Get和Put。从当前的池中获取临时对象,返回一个interface{}类型的值;在当前的池中存放临时对象,接受一个interface{}类型的值。
这个类型的Get方法可能会从当前的池中删除掉任何一个值,然后把这个值作为结果返回。如果此时当前的池中没有任何值,那么这个方法就会使用当前池的New字段创建一个新值,并直接将其返回。
特性:临时对象池可以把由其中的对象值产生的存储压力进行分摊,它会专门为每一个与操作它的Goroutine相关联的P都生成一个本地池;对垃圾回收友好,垃圾回收的执行一般会使临时对象池中的对象值被全部移除。
场景:不需要持久使用的某一类值,无需被区分,其中的任何一个值都可以代替另一个,因此可以将临时对象池当作针对某种临时且状态无关的数据的缓存来用
示例:
func main() {
    // 禁用GC,并保证在main函数执行结束前恢复GC
    defer debug.SetGCPercent(debug.SetGCPercent(-1))
    var count int32
    newFunc := func() interface{} {
        return atomic.AddInt32(&count, 1)
    }
    pool := sync.Pool{New: newFunc}

    // New 字段值的作用
    v1 := pool.Get()
    fmt.Printf("v1: %v\n", v1)

    // 临时对象池的存取
    pool.Put(newFunc())
    pool.Put(newFunc())
    pool.Put(newFunc())
    v2 := pool.Get()
    fmt.Printf("v2: %v\n", v2)

    // 垃圾回收对临时对象池的影响
    debug.SetGCPercent(100)
    runtime.GC()
    v3 := pool.Get()
    fmt.Printf("v3: %v\n", v3)
    pool.New = nil
    v4 := pool.Get()
    fmt.Printf("v4: %v\n", v4)
}
运行结果:
v1: 1
v2: 2
v3: 5
v4:

参考资料

《Go并发编程实战》

Go语言核心36讲

你可能感兴趣的:(go)