Go学习笔记06_(锁机制)

请注意,本文编写于 630 天前,最后修改于 191 天前,其中某些信息可能已经过时。

老马带你学习Go语言编程

初学笔记,如有错误.欢迎指正,不胜感激.

共享内存

goroutine之间的另外一种通信方式是共享内存,也就是访问相同的数据.但是只要两个或者两个以上的goroutine同时访问数据,并且至少有一个是写操作的时,就会发生数据竞争.解决数据竞争的方式是采用锁机制.在Go语言中主要通过sync包来实现.

sync.WaitGroup

WaitGroup可称为组等待,可以阻塞main goroutine的执行,直到所有其它的一组goroutine执行完成.

// 计数器增加delta,delta可以为负数
func (wg *WaitGroup) Add(delta int)
// 计数器减少1
func (wg *WaitGroup) Done()
// 等待计数器归零,如果计数器小于0,则该操作会引发panic
func (wg *WaitGroup) Wait()
package main

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

func main() {
    var wg = sync.WaitGroup{}
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(x int) {
            time.Sleep(time.Second)
            fmt.Println("第",x,"个goroutine执行完成")
            wg.Done()

        }(i)

    }
    wg.Wait()
    fmt.Println("程序执行完成")
}

上述代码如果不适用WaitGroup,则程序可能会不等我们创建的goroutine执行结束,直接执行主的goroutine打印程序退出.

使用wg.Wait()如果WaitGroup中的计数器不为0则会阻塞.我们在创建goroutine的时候执行wg.Add(1)每创建一个goroutine,WaitGroup内部的计数器会加1,当goroutine执行结束的时候执行wg.Done()WaitGroup内部的计数器减一,直到内部计数器为0的时候主goroutine才会继续执行,打印程序退出.

sync.Mutex 互斥锁

Mutex为互斥锁,Lock()加锁,Unlock()解锁.用于解决数据竞争问题.使用Lock()加锁后,不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.如果在使用Unlock()前未加锁,将引起一个panic异常.Mutex并不与特定的goroutine相关联,可以在一个goroutine中加锁,在另一个goroutine中解锁

package main

import (
    "fmt"
    "sync"
)

var sum int = 0
func main() {
    wg := sync.WaitGroup{}
    wg.Add(2)
    go func() {
        for i:= 0; i < 1e8; i++ {
            sum++
        }
        wg.Done()
    }()
    go func() {
        for i:= 0; i < 1e8; i++ {
            sum++
        }
        wg.Done()
    }()
    wg.Wait()
    fmt.Println(sum)
}

上述代码出现了数据竞争,所以最后打印出的值并不是确定2亿.

package main

import (
    "fmt"
    "sync"
)

var sum int = 0
func main() {
    wg := sync.WaitGroup{}
    wg.Add(2)
    var m = sync.Mutex{}
    go func() {
        for i:= 0; i < 1e8; i++ {
            m.Lock()
            sum++
            m.Unlock()
        }
        wg.Done()
    }()
    go func() {
        for i:= 0; i < 1e8; i++ {
            m.Lock()
            sum++
            m.Unlock()
        }
        wg.Done()
    }()
    wg.Wait()
    fmt.Println(sum)
}

由于两个goroutine会同时对sum进行改变引起数据竞争,所以我们在对sum进行操作的时候进行加锁和解锁操作,已消除数据竞争.

sync.RWMutex 读写锁

读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也是互斥的.但是,多个读操作之间却不存在互斥关系.常用于读此书远远多于写操作的场景,称为多读单写锁.他有四个常用方法:

// 写操作的锁定和解锁
Lock()
Unlock()
// 写操作的锁定和解锁
RLock()
RUnlock()
package main

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


var rw sync.RWMutex
func writeData(wg *sync.WaitGroup, id int) {

    rw.Lock()
    fmt.Println("第",id,"个读操作加锁")
    for i := 0; i < 5; i++ {
        fmt.Print("w")
        time.Sleep(time.Second)
    }

    fmt.Println("\n第",id,"个读操作解锁")
    rw.Unlock()
    wg.Done()
}

func readData(wg *sync.WaitGroup, id int) {
    rw.RLock()
    fmt.Println("第",id,"个写操作加锁")
    for i := 0; i < 6; i++ {
        fmt.Print("r")
        time.Sleep(time.Second)
    }
    fmt.Println("\n第",id,"个写操作解锁")
    rw.RUnlock()
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    for i:=0; i<3; i++ {
        wg.Add(1)
        go writeData(&wg, i)
    }
    for i:=0; i<6; i++ {
        wg.Add(1)
        go readData(&wg, i)
    }
    wg.Wait()
    fmt.Println("程序执行结束")
}

/*
打印结果:

第 1 个写操作加锁
wwwww
第 1 个写操作解锁
第 5 个读操作加锁
r第 2 个读操作加锁
r第 4 个读操作加锁
r第 1 个读操作加锁
第 3 个读操作加锁
rr第 0 个读操作加锁
rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
第 3 个读操作解锁

第 5 个读操作解锁

第 2 个读操作解锁

第 4 个读操作解锁

第 1 个读操作解锁

第 0 个读操作解锁
第 2 个写操作加锁
wwwww
第 2 个写操作解锁
第 0 个写操作加锁
wwwww
第 0 个写操作解锁
程序执行结束

Process finished with exit code 0

*/

通过上述结果我们可以发现开始所说的多个写操作是互斥的,多个读操作之间不存在互斥关系,读写之间也是互斥的.

sync.Once 初始化

Once的作用是多次调用但只执行一次,Once只有一个方法,Once.Do(),向Do传入一个函数,这个函数在第一指向Once.Do()的时候回被调用,以后再执行Once.Do()将没有任何动作.

package main

import (
    "fmt"
    "sync"
)

func onceBody() {
    fmt.Println("gogogogogo")
}

func main() {
    var wg sync.WaitGroup
    var once sync.Once
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            once.Do(onceBody)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("程序结束")
}

上述代码定义了十个goroutine,每一个goroutine中调用onceBody()函数.由于使用了sync.Once,所以本应该打印十次gogogogogo,而现在程序只打印了一次.

竞争条件检测

即使我们在编写代码时一再小心,但是并发程序中还是比较容易出错.幸运的事,GO的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器.使用时,只需要在go build,go run或者go test命令后面加上-race即可.

Comments

添加新评论