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