在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都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。
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 结构体实现了五种方法:
RWMutex的使用主要事项
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是一个比较冷门的结构体,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结束。
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 运行完成
$ 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
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报错。