互斥鎖 對於任一共用資源,同一時間保證只有一個操作者,這種方法稱為 互斥機制。 關鍵字 Mutex 表示互斥鎖類型,它的 Lock 方法用於獲取鎖,Unlock 方法用於釋放鎖。在 Lock 和 Unlock 之間的代碼,可以讀取和修改共用資源,這部分區域稱為 臨界區。 錯誤的併發操作 先來看一個錯 ...
互斥鎖
對於任一共用資源,同一時間保證只有一個操作者,這種方法稱為 互斥機制
。
關鍵字 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\n", 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)