一、兩個問題
1、同步執行問題
package main
import (
"fmt"
"time"
)
func main() {
go fun1()
go fun2()
fmt.Println("main函數等待")
time.Sleep(time.Second * 1)
fmt.Println("main函數結束")
}
func fun1() {
fmt.Println("fun1函數執行")
}
func fun2() {
fmt.Println("fun2函數執行")
}
主線程為了等待所有的子goroutine都運行完畢,不得不在程序中使用time.Sleep() 來睡眠一段時間,等待其他線程充分運行。這種方式耗費時間,顯然是不夠優雅的。
2、臨界資源問題
臨界資源: 指并發環境中多個進程/線程/協程共享的資源。并發編程中對臨界資源的處理不當, 往往會導致數據不一致的問題。
如果多個goroutine在訪問同一個數據資源(臨界資源)的時候,其中一個線程修改了數據,那么這個數值就被修改了,對于其他的goroutine來講,這個數值可能是不對的。
舉個例子,我們通過并發來實現火車站售票這個程序。一共有10張票,3個售票口同時出售。
package main
import (
"fmt"
"math/rand"
"time"
)
//全局變量票數
var tickets = 10
func main() {
//三個goroutine 模擬售票窗口
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
//為了保證3個goroutine協程正常工作,先將主線程睡眠5秒
time.Sleep(5 * time.Second)
}
func saleTickets(name string) {
//隨機數種子
rand.Seed(time.Now().UnixNano())
for {
if tickets > 0 {
//隨機睡眠1~1000ms
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Println(name, "余票:", tickets)
tickets--
} else {
fmt.Println(name, "售罄,已無票。。")
break
}
}
}
運行結果
售票口3 余票: 10
售票口2 余票: 10
售票口1 余票: 10
售票口3 余票: 7
售票口1 余票: 7
售票口3 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口2 余票: 3
售票口1 余票: 3
售票口1 售罄,已無票。。
售票口2 余票: 0
售票口2 售罄,已無票。。
售票口3 余票: -1
售票口3 售罄,已無票。。
在以上的代碼中,使用三個并發運行的go協程模擬了三個售票窗口同時售票,而由于全局變量tickets會被三個協程在一段時間內同時訪問,因此tickets就是我們所說的“臨界資源”。
我們可以發現:
在開始時,三個窗口同時讀到信息:tickets=10,從而隨機都輸出了余票=10
而在結尾時,竟然出現了余票為負數的情況,其產生的原因在于,票數快要賣完時,當售票口1余票1,并且售完這一張票后,在這個時間段內,售票口2已經進入了if tickets > 0滿足條件的代碼塊內,然而售票口1此時將最后一張票售出,tickets 由1變為0售票口2打印出來了不應該出現的結果:余票0,同理售票口3打印了不該出現的結果:余票-1。
多goroutine【多任務】,有共享資源,且多goroutine修改共享資源,出現數據不安全問題【數據錯誤】,保證數據安全一致,需要goroutine同步
goroutine同步方式:
- channel 【csp模型】
- sync包提供的方法
二、sync同步等待組WaitGroup
使用等待組進行多個任務的同步,等待組可以保證在并發環境中完成指定數量的任務。
等待組的方法:
方法名 | 功能 |
---|---|
(wg *WaitGroup)Add(delta int) | 等待組的計數器+1 |
(wg *WaitGroup)Done() | 等待組的計數器-1 |
(wg *WaitGroup)Wait() | 當等待組計數器不等于0時阻塞,直到為0 |
代碼示例:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(1)
go fun1()
wg.Add(1)
go fun2()
fmt.Println("main函數等待")
wg.Wait()
fmt.Println("main函數結束")
}
func fun1() {
fmt.Println("fun1函數執行")
wg.Done()
}
func fun2() {
fmt.Println("fun2函數執行")
wg.Done()
}
運行結果
main函數等待
fun1函數執行
fun2函數執行
main函數結束
三、sync互斥鎖Mutex
加鎖成功則操作資源,加鎖失敗則等待直至鎖加鎖成功——所有的goroutine互斥,一個得到鎖其他全部等待。
互斥鎖被稱為Mutex,它有2個函數,Lock()和Unlock()分別是獲取鎖和釋放鎖,如下:
type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}
修改上面售票代碼,解決臨界資源安全問題
示例代碼:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
//全局變量票數
var tickets = 10
var mutex sync.Mutex
var wg sync.WaitGroup
func main() {
//三個goroutine 模擬售票窗口
wg.Add(1)
go saleTickets("售票口1")
wg.Add(1)
go saleTickets("售票口2")
wg.Add(1)
go saleTickets("售票口3")
wg.Wait()
}
func saleTickets(name string) {
//隨機數種子
rand.Seed(time.Now().UnixNano())
for {
//上鎖
mutex.Lock()
if tickets > 0 {
//隨機睡眠1~1000ms
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
fmt.Println(name, "余票:", tickets)
tickets--
} else {
mutex.Unlock()
fmt.Println(name, "售罄,已無票。。")
break
}
//解鎖
mutex.Unlock()
}
wg.Done()
}
運行結果
售票口3 余票: 10
售票口3 余票: 9
售票口1 余票: 8
售票口2 余票: 7
售票口3 余票: 6
售票口1 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口1 余票: 2
售票口2 余票: 1
售票口1 售罄,已無票。。
售票口2 售罄,已無票。。
售票口3 售罄,已無票。。
四、sync讀寫鎖RWMutex
讀寫鎖要達到的效果是同一時間可以允許多個協程讀數據,但只能有且只有1個協程寫數據。也就是說,讀和寫是互斥的,寫和寫也是互斥的,但讀和讀并不互斥。
簡單來說:
- (1)可以隨便讀,多個goroutine同時讀。讀的時候不能寫。
- (2)寫的時候,啥也不能干。不能讀也不能寫。
讀寫鎖是RWMutex,它有5個函數:
- Lock()和Unlock()是給寫操作用的。
- RLock()和RUnlock()是給讀操作用的。
- RLocker()能獲取讀鎖,然后傳遞給其他協程使用。使用較少。
type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}
舉個例子,學生信息錄入系統,錄入學生信息是寫操作,讀取學生信息是讀操作。可以使用讀寫鎖:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
// Student 學生信息系統
type Student struct {
// 讀寫鎖
sync.RWMutex
// 存儲信息 姓名-年齡
data map[string]int
}
// Add 增加學生信息
func (s *Student) Add(name string, age int) {
defer wg.Done()
s.Lock()
defer s.Unlock()
if _, ok := s.data[name]; !ok {
s.data[name] = age
}
}
// Query 讀取學生信息
func (s *Student) Query(name string) {
defer wg.Done()
s.RLock()
defer s.RUnlock()
if v, ok := s.data[name]; ok {
fmt.Printf("姓名:%s\t年齡:%d\n", name, v)
} else {
fmt.Println("學生信息不存在!")
}
}
func main() {
s := &Student{
data: make(map[string]int),
}
wg.Add(4)
s.Add("jack", 20)
s.Add("tom", 23)
s.Add("lili", 18)
s.Add("lili", 20)
nameList := []string{"jack", "tom", "lili", "xiaohua"}
for _, v := range nameList {
wg.Add(1)
go s.Query(v)
}
wg.Wait()
}
運行結果
學生信息不存在!
姓名:jack 年齡:20
姓名:lili 年齡:18
姓名:tom 年齡:23
五、sync單次執行Once
sync.Once 是 Golang package 中使方法只執行一次的對象實現,作用與 init 函數類似。但也有所不同:
- init 函數是在文件包首次被加載的時候執行,且只執行一次
- sync.Once 是在代碼運行中需要的時候執行,且只執行一次
當一個函數不希望程序在一開始的時候就被執行的時候,我們可以使用 sync.Once 。
sync.Once是讓函數方法只被調用執行一次的實現,其最常應用于單例模式之下,例如初始化系統配置、保持數據庫唯一連接等。
代碼示例
package main
import (
"sync"
)
var configs map[string]string
func loadConfig() {
configs = map[string]string{
"url": "http://www.lxweimin.com",
"id": "cd41c8c3645c",
"email": "everydawn@jianshu.com",
}
}
// Config1 被多個goroutine調用時不是并發安全的
// 比如有兩個線程都在調用Config1函數,線程A在執行到if configs==nil后
// cpu切換到線程B執行,直到線程B運行完,這時configs已經被實例化,
// 當cpu在切回到線程A繼續執行的時候,對configs又執行實例化操作,
// 這時內存中已有configs的兩個實例,違背了單例定義。
func Config1(name string) string {
if configs == nil {
loadConfig()
}
return configs[name]
}
var loadConfigOnce sync.Once
// Config2 是并發安全的
func Config2(name string) string {
loadConfigOnce.Do(loadConfig)
return configs[name]
}
func main() {
}