Go 快速入门指南 – 互斥锁和定时器
互斥锁
对于任一共享资源,同一时间保证只有一个操作者,这种方法称为 互斥机制
。
关键字 Mutex
表示互斥锁类型,它的 Lock
方法用于获取锁,Unlock
方法用于释放锁。在 Lock
和 Unlock
之间的代码,可以读取和修改共享资源,这部分区域称为 临界区
。
错误的并发操作
先来看一个错误的示例。
在 Map 小节中讲到, Map
不是并发安全的, 也就是说,如果在多个线程中,同时对一个 Map 进行读写,会报错。现在来验证一下, 通过启动 100 个 goroutine
来模拟并发调用,每个 goroutine 都对 Map 的 key 进行设置。
package main
import "sync"
func main() {
m := make(map[int]bool)
var wg sync.WaitGroup
for j := 0; j < 100; j++ {
wg.Add(1)
go func(key int) {
defer func() {
wg.Done()
}()
m[key] = true // 对 Map 进行并发写入
}(j)
}
wg.Wait()
}
// $ go run main.go
// 输出如下,报错信息
/**
fatal error: concurrent map writes
fatal error: concurrent map writes
goroutine 104 [running]:
main.main.func1(0x0?)
/home/codes/Go-examples-for-beginners/main.go:18 +0x66
created by main.main
/home/codes/Go-examples-for-beginners/main.go:13 +0x45
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc0000112c0?)
/usr/local/go/src/runtime/sema.go:62 +0x25
sync.(*WaitGroup).Wait(0x60?)
/usr/local/go/src/sync/waitgroup.go:139 +0x52
main.main()
/home/codes/Go-examples-for-beginners/main.go:22 +0x105
...
...
...
*/
通过输出信息 fatal error: concurrent map writes
可以看到,并发写入 Map 确实会报错。
正确的并发操作
Map 并发写入如何正确地实现呢?
一种简单的方案是在并发临界区域 (也就是设置 Map key 的地方) 进行加互斥锁操作, 互斥锁保证了同一时刻 只有一个 goroutine 获得锁,其他 goroutine 全部处于等待状态,这样就把并发写入变成了串行写入, 从而消除了报错问题。
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
m := make(map[int]bool)
var wg sync.WaitGroup
for j := 0; j < 100; j++ {
wg.Add(1)
go func(key int) {
defer func() {
wg.Done()
}()
mu.Lock() // 写入前加锁
m[key] = true // 对 Map 进行并发写入
mu.Unlock() // 写入完成解锁
}(j)
}
wg.Wait()
fmt.Printf("Map size = %d
", len(m))
}
// $ go run main.go
// 输出如下
/**
Map size = 100
*/
超时控制
利用 channel (通道)
和 time.After()
方法实现超时控制。
例子
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan bool)
go func() {
defer func() {
ch <- true
}()
time.Sleep(2 * time.Second) // 模拟超时操作
}()
select {
case <-ch:
fmt.Println("ok")
case <-time.After(time.Second):
fmt.Println("timeout!")
}
}
// $ go run main.go
// 输出如下
/**
timeout!
*/
定时器
调用 time.NewTicker
方法即可。
例子
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
done := make(chan bool)
go func() {
time.Sleep(5 * time.Second) // 模拟耗时操作
done <- true
}()
for {
select {
case <-done:
fmt.Println("Done!")
return
case <-ticker.C:
fmt.Println(time.Now().Format("2006-01-02 15:04:05"))
}
}
}
// $ go run main.go
// 输出如下,你的输出可能和这里的不一样
/**
2021-01-03 15:40:21
2021-01-03 15:40:22
2021-01-03 15:40:23
2021-01-03 15:40:24
2021-01-03 15:40:25
Done!
*/
扩展阅读
-
1. 互斥锁 – 维基百科 (https://zh.wikipedia.org/wiki/互斥锁)
-
2. 临界区 – 百度百科 (https://baike.baidu.com/item/临界区/8942134)