在go语言多线程编程的过程中,我们会遇到多线程进行资源读写的问题,在GO语言中我们可以使用channel进行控制,但是除了channel我们还可以通过sync库进行资源的读写控制,这也就是我们常说的锁。锁的作用就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。本文向记录了学习sync标准库的学习笔记,希望对你有帮助。
这里摘录百度百科的解释 :
互斥锁是用来保证共享数据操作的完整性的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
在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在使用后不能被复制,因此这里我们要注意。
接下来我们看一下如何使用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通过比对两种情况大家应该理解了互斥锁的使用了。
同样这里我引用百度百科的解释:
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
sync.RWMutex 结构体实现了五种方法:
func (rw *RWMutex) Lock() 写锁定func (rw *RWMutex) UnLock() 写解锁func (rw *RWMutex) RLock() 读锁定func (rw *RWMutex) RUnLock() 读解锁func (rw *RWMutex) RLocker() LockerRWMutex的使用主要事项
读锁的时候无需等待读锁的结束读锁的时候要等待写锁的结束写锁的时候要等待读锁的结束写锁的时候要等待写锁的结束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是一个比较冷门的结构体,sync.Cond用于goroutine之间的协作,用于协程的挂起和唤醒。
从下面的结构体我么可以看出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实例是否被复制 }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对象。下面的示例代码中我们使用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后,往往通过延时的方式等待goroutine退出,这种方式是比较耗费时间的,在sync中我们可以使用WaitGroup的方法更优雅的等待goroutine结束。
Output:
$ go run main.go goroutine 2 运行完成 goroutine 0 运行完成 goroutine 1 运行完成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报错。
Go语言标准库学习之sync二(通过sync.Pool大幅度提升程序运行性能)