Go语言标准库学习之sync一(go语言中的锁)

    技术2022-07-13  72

    在go语言多线程编程的过程中,我们会遇到多线程进行资源读写的问题,在GO语言中我们可以使用channel进行控制,但是除了channel我们还可以通过sync库进行资源的读写控制,这也就是我们常说的锁。锁的作用就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。本文向记录了学习sync标准库的学习笔记,希望对你有帮助。

    一、互斥锁

    1.什么是互斥锁

    这里摘录百度百科的解释 :

    互斥锁是用来保证共享数据操作的完整性的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

    2.sync.Mutex

    在sync库中Mutex对象实现了两个方法,Lock和UnLock,从字面意思就可以理解,一个是锁另一个是释放锁。

    // 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在使用后不能被复制,因此这里我们要注意。

    3.互斥锁的使用

    接下来我们看一下如何使用Mutex进行资源锁定和释放。

    package main import ( "fmt" "sync" "time" ) // 定义一个锁 var m = new(sync.Mutex) func StdOut(s string) { // 创建一个互斥锁 //m := new(sync.Mutex) m.Lock() // 当main函数执行完成后,释放锁 defer m.Unlock() for _, data := range s { fmt.Printf("%c", data) } fmt.Println() } func Person1(s string) { StdOut(s) } func main() { go Person1("Random_w1") go Person1("Random_w2") Person1("Random_w3") // 等待两秒,让goroutine运行完成 time.Sleep(time.Millisecond * 100) }

    Output:

    $ go run main.go Random_w3 Random_w1 Random_w2

    如果将StdOut中的Lock删除掉,那么输出就会混乱:

    $ go run main.go RandomRandom_w1 Random_w2 _w3

    通过比对两种情况大家应该理解了互斥锁的使用了。

    二、读写锁

    1.什么是读写锁

    同样这里我引用百度百科的解释:

    读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

    互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

    2. sync.RWMutex

    type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers readerSem uint32 // semaphore for readers to wait for completing writers readerCount int32 // number of pending readers readerWait int32 // number of departing readers }

    sync.RWMutex 结构体实现了五种方法:

    func (rw *RWMutex) Lock() 写锁定func (rw *RWMutex) UnLock() 写解锁func (rw *RWMutex) RLock() 读锁定func (rw *RWMutex) RUnLock() 读解锁func (rw *RWMutex) RLocker() Locker

    RWMutex的使用主要事项

    读锁的时候无需等待读锁的结束读锁的时候要等待写锁的结束写锁的时候要等待读锁的结束写锁的时候要等待写锁的结束

    3. 读写锁的使用

    package main import ( "fmt" "math/rand" "sync" "time" ) // 定义一个锁 var m = new(sync.RWMutex) var count int // Write 对count进行写操作 func Write(n int) { rand.Seed(time.Now().UnixNano()) fmt.Printf("写 goroutine %d 正在写数据...\n", n) m.Lock() num := rand.Intn(500) count = num fmt.Printf("写 goroutine %d 写数据结束,写入新值 %d\n", n, num) m.Unlock() } // Read 对count进行读操作 func Read(n int) { m.RLock() fmt.Printf("读 goroutine %d 正在读取数据...\n", n) num := count fmt.Printf("读 goroutine %d 读取数据结束,读到 %d\n", n, num) m.RUnlock() } func main() { // 创建goroutine,进行读写操作 for i := 0; i < 3; i++ { go Read(i) go Write(i) } time.Sleep(time.Second) }

    Output:

    $ go run main.go 读 goroutine 1 正在读取数据... 读 goroutine 1 读取数据结束,读到 0 读 goroutine 0 正在读取数据... 读 goroutine 0 读取数据结束,读到 0 写 goroutine 2 正在写数据... 写 goroutine 0 正在写数据... 写 goroutine 1 正在写数据... 读 goroutine 2 正在读取数据... 读 goroutine 2 读取数据结束,读到 0 写 goroutine 2 写数据结束,写入新值 337 写 goroutine 0 写数据结束,写入新值 16 写 goroutine 1 写数据结束,写入新值 134

    从Output中我们可以看到,当读锁被锁定时,写锁时阻塞状态,只有当读锁解除后,count才能写入新值。

    三、Cond的使用

    Cond是一个比较冷门的结构体,sync.Cond用于goroutine之间的协作,用于协程的挂起和唤醒。

    1. Cond结构体

    从下面的结构体我么可以看出Cond在被创建后是不能复制的,和互斥锁类似。

    // A Cond must not be copied after first use. type Cond struct { noCopy noCopy // noCopy可以嵌入到结构中,在第一次使用后不可复制,使用go vet作为检测使用 L Locker // 根据需求初始化不同的锁,如*Mutex 和 *RWMutex notify notifyList // 通知列表,调用Wait()方法的goroutine会被放入list中,每次唤醒,从这里取出 checker copyChecker // 复制检查,检查cond实例是否被复制 }

    2. Cond结构体实现的方法

    Cond结构体实现了四个方法,分别是:

    func (c *Cond) Wait() 必须获取该锁之后才能调用Wait()方法,Wait方法在调用时会释放底层锁Locker,并且将当前goroutine挂起,直到另一个goroutine执行Signal或者Broadcase,该goroutine才有机会重新唤醒,并尝试获取Locker,完成后续逻辑。也就是在等待被唤醒的过程中是不占用锁Locker的,这样就可以有多个goroutine可以同时处于Wait(等待被唤醒的状态)func (c *Cond) Signal() 唤醒等待队列中的一个goroutine,一般都是任意唤醒队列中的一个goroutine。func (c *Cond) Broadcast()唤醒等待队列中的所有goroutine。func NewCond(l Locker) *Cond ,使用Locker创建一个Cond对象。

    3. Cond的使用

    下面的示例代码中我们使用cond.Wait让goroutine进入等待状态,在main函数中,我们分别测试了使用Siginal和Broadcast将goroutine唤醒,为了表示Siginal一次只能唤醒一个goroutine因此加入了时间,正常情况下实例中的goroutine在不到一微秒的时间就可以执行完成,但是我们延时了一秒,除了被唤醒的goroutine运行外,其他goroutine并没有执行。使用Broadcast我们可以看到剩下的两个goroutine快速执行完成。

    package main import ( "fmt" "sync" "time" ) // 定义一个锁 var mutex = new(sync.Mutex) // 初始化一个cond var cond = sync.NewCond(mutex) func CondTest() { // 5个goroutine正常情况下一微秒时间都可以运行完 for i := 0; i < 5; i++ { id := i go func() { mutex.Lock() defer mutex.Unlock() // 让所有goroutine等待 cond.Wait() fmt.Printf("goroutine %d 运行完成\n", id) }() } } // 输出时间 func PrintTime() { fmt.Println(time.Now().Format("15:04:05")) } func main() { CondTest() // 运行三个goroutine for i := 0; i < 3; i++ { PrintTime() cond.Signal() time.Sleep(time.Second) } // 通过Broadcast唤醒所有的goroutine PrintTime() cond.Broadcast() time.Sleep(time.Millisecond) }

    Output:

    $ go run main.go 14:40:29 goroutine 1 运行完成 14:40:30 goroutine 0 运行完成 14:40:31 goroutine 2 运行完成 14:40:32 goroutine 4 运行完成 goroutine 3 运行完成

    四、更优雅的等待goroutine结束(sync.WaitGroup)

    平时我们在测试或者创建goroutine后,往往通过延时的方式等待goroutine退出,这种方式是比较耗费时间的,在sync中我们可以使用WaitGroup的方法更优雅的等待goroutine结束。

    1. WaitGroup的使用

    package main import ( "fmt" "sync" "time" ) func main() { // 新建一个WaitGroup对象 wg := sync.WaitGroup{} // WaitGroup的数量为3 wg.Add(3) for i := 0; i < 3; i++ { id := i go func() { fmt.Printf("goroutine %d 运行完成\n", id) // goroutine运行完成,通知wg wg.Done() }() } //wg程序阻塞,等待所有goroutine运行完成 wg.Wait() }

    Output:

    $ go run main.go goroutine 2 运行完成 goroutine 0 运行完成 goroutine 1 运行完成

    2. WaitGroup的注意事项

    我们不能使用Add() 给wg 设置一个负值,否则代码将会报错。 $ go run main.go panic: sync: negative WaitGroup counter goroutine 1 [running]: sync.(*WaitGroup).Add(0xc000070070, 0xffffffffffffffff) c:/Go/src/sync/waitgroup.go:74 +0x13c main.main() D:/GOCODE/Test/main.go:12 +0x54 exit status 2 WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址。 package main import ( "fmt" "math/rand" "sync" "time" ) func main() { wg := sync.WaitGroup{} wg.Add(3) for i := 0; i < 3; i++ { id := i go func(wg *sync.WaitGroup) { fmt.Printf("goroutine %d 运行完成\n", id) wg.Done() }(&wg) } wg.Wait() }

    Output:

    $ go run main.go goroutine 2 运行完成 goroutine 0 运行完成 goroutine 1 运行完成

    上面的main函数如果改成这样:

    package main import ( "fmt" "math/rand" "sync" "time" ) func main() { wg := sync.WaitGroup{} wg.Add(3) for i := 0; i < 3; i++ { id := i go func(wg sync.WaitGroup) { fmt.Printf("goroutine %d 运行完成\n", id) wg.Done() }(wg) } wg.Wait() }

    Output:

    $ go run main.go goroutine 2 运行完成 goroutine 0 运行完成 goroutine 1 运行完成 fatal error: all goroutines are asleep - deadlock! goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000070078) c:/Go/src/runtime/sema.go:56 +0x40 sync.(*WaitGroup).Wait(0xc000070070) c:/Go/src/sync/waitgroup.go:130 +0x6c main.main() D:/GOCODE/Test/main.go:20 +0xbc exit status 2

    可以看到,不使用地址传递参数会在goroutine运行完成之后触发panic报错。

    下一篇文章向大家介绍sync.Pool:

    Go语言标准库学习之sync二(通过sync.Pool大幅度提升程序运行性能)

    Processed: 0.010, SQL: 9