golang并發(fā)模型
- go在語(yǔ)言層面提供了內(nèi)置的并發(fā)支持
- 不要通過(guò)共享內(nèi)存來(lái)通信,而應(yīng)該通過(guò)通信來(lái)共享內(nèi)存
并發(fā)與并行
- 定義
- 并發(fā): 指同一時(shí)刻, 系統(tǒng)通過(guò)調(diào)度,來(lái)回切換交替的運(yùn)行多個(gè)任務(wù),看起來(lái)是"同時(shí)"進(jìn)行的.一個(gè)處理器同時(shí)處理多個(gè)任務(wù)
- 并行:指同一時(shí)刻,2個(gè)任務(wù)"真正的"同時(shí)進(jìn)行.多個(gè)處理器或者多核的處理器同時(shí)處理多個(gè)不同的任務(wù).
- 并行:多核cpu,物理上的同時(shí)執(zhí)行.
image
- 并發(fā):同一時(shí)刻,只能一條指令執(zhí)行.通過(guò)快速輪換執(zhí)行,宏觀上是多個(gè)線程同時(shí)執(zhí)行的.微觀上不是同時(shí)執(zhí)行的,只是把時(shí)間分成若干段,使得多個(gè)線程快速交替執(zhí)行.(單核cpu,邏輯上的同時(shí)執(zhí)行)
image
常見(jiàn)的并發(fā)編程模型
- 進(jìn)程&線程(Apache)
最初的web服務(wù)器都是基于進(jìn)程和線程,比如Apache,新到的一個(gè)請(qǐng)求就會(huì)分配一個(gè)進(jìn)程或者線程,每個(gè)進(jìn)程只服務(wù)一個(gè)用戶,早期的互聯(lián)網(wǎng)還不夠普及,用戶也不夠多,這時(shí)候網(wǎng)站是可以穩(wěn)定的,問(wèn)題是
進(jìn)程很昂貴,一臺(tái)服務(wù)器無(wú)法創(chuàng)建很多的進(jìn)程,后來(lái)隨著互聯(lián)網(wǎng)的發(fā)展,用戶越來(lái)越多,
網(wǎng)站也變得越來(lái)越復(fù)雜,一個(gè)頁(yè)面有可能就有上百個(gè)請(qǐng)求,
所以就誕生了C10K的問(wèn)題,C10K的意思就是服務(wù)器同時(shí)支持一個(gè)10k量級(jí)的并發(fā)連接,就是要?jiǎng)?chuàng)建1w個(gè)進(jìn)程,
這樣的話,操作系統(tǒng)肯定是無(wú)法承受的.
所以進(jìn)程和線程模型就顯得很力不從心了.
- 異步非阻塞(Nginx)
為了解決c10k的問(wèn)題,就發(fā)明了異步非阻塞這種技術(shù),一個(gè)典型的案例就是linux里的epoll,
一個(gè)普通的服務(wù)器就能服務(wù)大量的用戶,資源消耗也很低,像NGINX都是epoll的產(chǎn)物,
但是,異步非阻塞也不是很完美,為了追求性能,強(qiáng)行的將線性程序打亂,開(kāi)發(fā)和維護(hù)都變得非常復(fù)雜,
調(diào)試起來(lái)也比較困難.
- 協(xié)程(Golang)
為了降低開(kāi)發(fā)的復(fù)雜度,讓程序員同學(xué)更爽的寫(xiě)代碼,協(xié)程這種并發(fā)模型就逐漸流行起來(lái),
這種模型,可以讓我們像寫(xiě)線性程序一樣,來(lái)寫(xiě)異步的程序,
其實(shí)協(xié)程的底層就是線程,但它比線程更輕量,幾十個(gè)協(xié)程,體現(xiàn)在底層,可能也就是五六個(gè)的線程.
大家把協(xié)程理解成,更高效,更易用,更輕量的線程.
Glang并發(fā)的實(shí)現(xiàn)
- 程序并發(fā)執(zhí)行(goroutine)
每開(kāi)啟一個(gè)協(xié)程,就會(huì)有一個(gè)goroutine,負(fù)責(zé)程序的并發(fā)執(zhí)行
f1() // 執(zhí)行函數(shù)f1,等待函數(shù)f1返回
go f1() // 執(zhí)行函數(shù)f1
f2() // 不用等待f1()的返回
使用起來(lái)很簡(jiǎn)單, 只要在一個(gè)函數(shù)前面加 go 關(guān)鍵字就會(huì)創(chuàng)建一個(gè)goroutine, 去并發(fā)的執(zhí)行,
所以程序并不會(huì)阻塞, 最后這2個(gè)函數(shù)相當(dāng)于會(huì)去并發(fā)的執(zhí)行.
有了goroutine之后就可以并發(fā)的去執(zhí)行了,這就引出了一個(gè)問(wèn)題:在多個(gè)goroutine之間是如何進(jìn)行數(shù)據(jù)通信的呢?
比如 go f1() 和 f2() 這2個(gè)函數(shù)之間要通信,要傳遞數(shù)據(jù)的話,那他們是怎么進(jìn)行的?
使用channel在多個(gè)goroutine之間進(jìn)行數(shù)據(jù)通信和同步的
- 多個(gè)goroutine間的數(shù)據(jù)同步和通信(channel)
- channel的基礎(chǔ)語(yǔ)法
- 創(chuàng)建1:make(chan [type]) // 無(wú)緩沖
- 創(chuàng)建2:make(chan [type], int) // 有緩沖
- 寫(xiě)入:channel <-
- 獲取: <- channel
- channel的基礎(chǔ)語(yǔ)法
c := make(chan string) // 聲明一個(gè)無(wú)緩沖的channel
// 創(chuàng)建一個(gè)goroutine
go func() {
c <- "this is channel msg " // 發(fā)送數(shù)據(jù)到channel里
}()
msg := <-c // 阻塞直到接收到數(shù)據(jù)
fmt.Println(msg)
說(shuō)明:
// channel分為無(wú)緩沖信道(即unbuffered channel)和有緩沖信道(buffered channel)。
無(wú)緩沖的與有緩沖channel有著重大差別:一個(gè)是同步的 一個(gè)是非同步的
ch1:=make(chan int) 無(wú)緩沖
ch2:=make(chan int,1) 有緩沖
ch1<-1 // 無(wú)緩沖的
這里要有別的協(xié)程一直<-ch1 接受這個(gè)參數(shù),
那么ch1<-1之后的代碼才能執(zhí)行,要不然就一直阻塞著,
ch2<-1 // 有緩沖的
這里則不會(huì)阻塞,因?yàn)榫彌_大小是1(放了一個(gè)緩沖就剩0了),只有當(dāng)放第二個(gè)的時(shí)候,第一個(gè)還沒(méi)被拿走,這時(shí)候才會(huì)阻塞.
比喻:
1. 無(wú)緩沖的就是一個(gè)送信人去你家門口送信,你不在家他不走,一定要送到你手里他才走.
無(wú)緩沖保證信能到你手上.
2. 有緩沖的就是一個(gè)送信人去你家門口送信,扔到你家信箱轉(zhuǎn)身就走,除非你的信箱滿了,他必須等信箱空下來(lái).
有緩沖保證信能到你家信箱
- 多個(gè)channel選擇數(shù)據(jù)讀取或者寫(xiě)入(select)
使用select關(guān)鍵字,完成“多路選擇”與“超時(shí)控制”。
select {
case v := <-ch1:
fmt.Println("channel 1 msg =>", v)
case v := <-ch2:
fmt.Println("channel 2 msg =>", v)
case <-time.After(time.Millisecond * 100): // 超時(shí)等待
fmt.Println("time out")
//default:
// fmt.Println("nothing")
}
使用場(chǎng)景:可以監(jiān)聽(tīng)寫(xiě)信號(hào),進(jìn)程的熱啟動(dòng),配置的熱加載等
協(xié)程的使用
func TestGroutine(t *testing.T) {
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i) // 正確案例,值傳遞。各個(gè)協(xié)程無(wú)競(jìng)爭(zhēng)關(guān)系。
}(i)
// go func() {
// fmt.Println(i) // 錯(cuò)誤案例,共享變量。各個(gè)協(xié)程有競(jìng)爭(zhēng)關(guān)系, 不安全
// }()
}
time.Sleep(time.Millisecond * 50)
}
- 協(xié)程并發(fā),導(dǎo)致協(xié)程不安全
// 協(xié)程不安全demo
func TestThreadUnsafe(t *testing.T) {
counter := 0
for i := 0; i < 5000; i++ {
go func() {
counter++
}()
}
time.Sleep(1 * time.Second)
t.Logf("counter = %d", counter)
}
// 輸出結(jié)果如下:
=== RUN TestThreadUnsafe
channel_test.go:346: counter = 4742 // 計(jì)算錯(cuò)誤,因?yàn)椴l(fā)導(dǎo)致了漏值
--- PASS: TestThreadUnsafe (1.00s)
- 如何保證協(xié)程安全?
- 方式1: 普通加鎖,并延遲等待協(xié)程執(zhí)行完畢(不推薦)
// 協(xié)程等待demo(停1秒,不推薦)
func TestThreadSafe(t *testing.T) {
var mut sync.Mutex // 互斥鎖
counter := 0
for i := 0; i < 5000; i++ {
go func() { // 開(kāi)啟協(xié)程
defer func() {
mut.Unlock() //函數(shù)調(diào)用完成后:解鎖,保證協(xié)程安全
}()
mut.Lock() // 函數(shù)將要調(diào)用前:加鎖,保證協(xié)程安全
counter++
}()
}
time.Sleep(1 * time.Second) // 等待一秒,等協(xié)程全部執(zhí)行完(如果程序復(fù)雜,1s可能不夠用)
t.Logf("counter = %d", counter)
}
// 輸出結(jié)果如下:
=== RUN TestThreadSafe
channel_test.go:363: counter = 5000
// 結(jié)果正確,但是有一個(gè)問(wèn)題。因?yàn)檫@里有個(gè)1秒的延遲等待,保證協(xié)程運(yùn)行完畢再調(diào)用結(jié)果
--- PASS: TestThreadSafe (1.00s)
- 先來(lái)介紹下:同步等待組(WaitGroup)
waitGroup用于同步協(xié)程同步,等待一組協(xié)程執(zhí)行完畢,才會(huì)繼續(xù)向下執(zhí)行.
1. 主協(xié)程調(diào)用Add()設(shè)置等待的協(xié)程數(shù)量.
2. 協(xié)程執(zhí)行完畢,調(diào)用Done()函數(shù)
3. wait()函數(shù)阻塞,直到所有協(xié)程執(zhí)行完畢才會(huì)繼續(xù)向下執(zhí)行.
- 方法2 : 使用同步等待隊(duì)列(waitGroup)保證順序執(zhí)行
// 協(xié)程安全Demo
func TestWaitGroup(t *testing.T) {
var mut sync.Mutex // 互斥鎖
var wg sync.WaitGroup // 等待隊(duì)列
counter := 0
for i := 0; i < 5000; i++ {
wg.Add(1) // 加個(gè)任務(wù)
go func() {
defer func() {
mut.Unlock() //函數(shù)調(diào)用完成后:解鎖,保證協(xié)程安全
}()
mut.Lock() // 函數(shù)將要調(diào)用前:加鎖,保證協(xié)程安全
counter++
wg.Done() // 做完任務(wù)
}()
}
wg.Wait() //等待所有任務(wù)執(zhí)行完畢
t.Logf("counter = %d", counter)
}
// 運(yùn)行結(jié)果如下:
=== RUN TestWaitGroup
channel_test.go:382: counter = 5000
--- PASS: TestWaitGroup (0.00s)
channel的關(guān)閉和廣播
- close 內(nèi)置函數(shù)關(guān)閉一個(gè)channel,該通道必須是雙向的或僅發(fā)送的
ch1 := make(chan int, 1)
ch2 := make(chan<- int, 1)
ch3 := make(<-chan int, 1)
close(ch1)
close(ch2)
close(ch3) // 報(bào)錯(cuò) invalid operation: close(ch3) (cannot close receive-only channel)
- 向已經(jīng)關(guān)閉的channel發(fā)送數(shù)據(jù)會(huì)panic
- 關(guān)閉一個(gè)已經(jīng)關(guān)閉的channel會(huì)panic
- v, ok <- channel。 其中,ok為bool值,若ok == true時(shí),表示channel處于open狀態(tài)。 若ok==false時(shí),表示channel處于close狀態(tài)。
常見(jiàn)的并發(fā)場(chǎng)景
- 只執(zhí)行一次(單例模式)
func TestOnceDo(t *testing.T) {
var once sync.Once
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
// 在多協(xié)程的情況下,保證某段代碼只執(zhí)行一次。
go func(ii int) {
once.Do(func() {
t.Log(ii)
})
wg.Done()
}(i)
}
wg.Wait()
}
// 輸出結(jié)果
=== RUN TestOnceDo
channel_test.go:404: 0
--- PASS: TestOnceDo (0.00s)
- 發(fā)郵件,發(fā)短信
- 跑腳本
- 爬數(shù)據(jù)
總結(jié)
- 關(guān)鍵字
- go
- make
- chan
- select
- sync下的包
- 互斥鎖
- 讀寫(xiě)鎖
- waitGroup
- map等
- 將復(fù)雜的任務(wù)拆分, 讓goroutine去并發(fā)的執(zhí)行,通過(guò)channel做數(shù)據(jù)通信