前言
如果說(shuō)php是最好的語(yǔ)言麻削,那么golang就是最并發(fā)的語(yǔ)言沟启。
支持golang的并發(fā)很重要的一個(gè)是goroutine的實(shí)現(xiàn)读规,那么本文將重點(diǎn)圍繞goroutine來(lái)做一下相關(guān)的筆記链峭,以便日后快速留戀拙徽。
10s后伸辟,以下知識(shí)點(diǎn)即將靠近:
1.從并發(fā)模型說(shuō)起
2.goroutine的簡(jiǎn)介
3.goroutine的使用姿勢(shì)
4.通道(channel)的簡(jiǎn)介
5.重要的四種通道使用
6.goroutine死鎖與處理
7.select的簡(jiǎn)介
8.select的應(yīng)用場(chǎng)景
9.select死鎖
正文
1.從并發(fā)模型說(shuō)起
看過(guò)很多大神簡(jiǎn)介幕侠,各種研究高并發(fā)靶累,那么就通俗的說(shuō)下并發(fā)侵歇。
并發(fā)目前來(lái)看比較主流的就三種:
1.多線程
每個(gè)線程一次處理一個(gè)請(qǐng)求骂澄,線程越多可并發(fā)處理的請(qǐng)求數(shù)就越多,但是在高并發(fā)下惕虑,多線程開(kāi)銷(xiāo)會(huì)比較大坟冲。
2.協(xié)程
無(wú)需搶占式的調(diào)度,開(kāi)銷(xiāo)小溃蔫,可以有效的提高線程的并發(fā)性健提,從而避免了線程的缺點(diǎn)的部分
3.基于異步回調(diào)的IO模型
說(shuō)一個(gè)熟悉的,比如nginx使用的就是epoll模型伟叛,通過(guò)事件驅(qū)動(dòng)的方式與異步IO回調(diào)私痹,使得服務(wù)器持續(xù)運(yùn)轉(zhuǎn),來(lái)支撐高并發(fā)的請(qǐng)求
為了追求更高效和低開(kāi)銷(xiāo)的并發(fā)统刮,golang的goroutine來(lái)了紊遵。
2.goroutine的簡(jiǎn)介
定義:在go里面,每一個(gè)并發(fā)執(zhí)行的活動(dòng)成為goroutine侥蒙。
詳解:goroutine可以認(rèn)為是輕量級(jí)的線程暗膜,與創(chuàng)建線程相比,創(chuàng)建成本和開(kāi)銷(xiāo)都很小鞭衩,每個(gè)goroutine的堆棧只有幾kb学搜,并且堆椡奚疲可根據(jù)程序的需要增長(zhǎng)和縮小(線程的堆棧需指明和固定),所以go程序從語(yǔ)言層面支持了高并發(fā)瑞佩。
程序執(zhí)行的背后:當(dāng)一個(gè)程序啟動(dòng)的時(shí)候聚磺,只有一個(gè)goroutine來(lái)調(diào)用main函數(shù),稱(chēng)它為主goroutine炬丸,新的goroutine通過(guò)go語(yǔ)句進(jìn)行創(chuàng)建瘫寝。
3.goroutine的使用姿勢(shì)
3.1單個(gè)goroutine創(chuàng)建
在函數(shù)或者方法前面加上關(guān)鍵字go,即創(chuàng)建一個(gè)并發(fā)運(yùn)行的新goroutine御雕。
上代碼:
package main
import (
"fmt"
"time"
)
func HelloWorld() {
fmt.Println("Hello world goroutine")
}
func main() {
go HelloWorld() // 開(kāi)啟一個(gè)新的并發(fā)運(yùn)行
time.Sleep(1*time.Second)
fmt.Println("我后面才輸出來(lái)")
}
以上執(zhí)行后會(huì)輸出:
Hello world goroutine
我后面才輸出來(lái)
需要注意的是矢沿,執(zhí)行速度很快滥搭,一定要加sleep酸纲,不然你一定可以看到goroutine里頭的輸出。
這也說(shuō)明了一個(gè)關(guān)鍵點(diǎn):當(dāng)main函數(shù)返回時(shí)瑟匆,所有的gourutine都是暴力終結(jié)的闽坡,然后程序退出。
3.2多個(gè)goroutine創(chuàng)建
package main
import (
"fmt"
"time"
)
func DelayPrint() {
for i := 1; i <= 4; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Println(i)
}
}
func HelloWorld() {
fmt.Println("Hello world goroutine")
}
func main() {
go DelayPrint() // 開(kāi)啟第一個(gè)goroutine
go HelloWorld() // 開(kāi)啟第二個(gè)goroutine
time.Sleep(2*time.Second)
fmt.Println("main function")
}
函數(shù)輸出:
Hello world goroutine
1
2
3
4
5
main function
1
2
3
4
5
6
7
有心的同學(xué)可能會(huì)發(fā)現(xiàn)愁溜,DelayPrint里頭有sleep疾嗅,那么會(huì)導(dǎo)致第二個(gè)goroutine堵塞或者等待嗎?
答案是:no
疑惑:當(dāng)程序執(zhí)行g(shù)o FUNC()的時(shí)候冕象,只是簡(jiǎn)單的調(diào)用然后就立即返回了代承,并不關(guān)心函數(shù)里頭發(fā)生的故事情節(jié),所以不同的goroutine直接不影響渐扮,main會(huì)繼續(xù)按順序執(zhí)行語(yǔ)句论悴。
4.通道(channel)的簡(jiǎn)介
4.1簡(jiǎn)介
如果說(shuō)goroutine是Go并發(fā)的執(zhí)行體,那么”通道”就是他們之間的連接墓律。
通道可以讓一個(gè)goroutine發(fā)送特定的值到另外一個(gè)goroutine的通信機(jī)制膀估。
4.2聲明&傳值&關(guān)閉
聲明
var ch chan int // 聲明一個(gè)傳遞int類(lèi)型的channel
ch := make(chan int) // 使用內(nèi)置函數(shù)make()定義一個(gè)channel
//=========
ch <- value // 將一個(gè)數(shù)據(jù)value寫(xiě)入至channel,這會(huì)導(dǎo)致阻塞耻讽,直到有其他goroutine從這個(gè)channel中讀取數(shù)據(jù)
value := <-ch // 從channel中讀取數(shù)據(jù)察纯,如果channel之前沒(méi)有寫(xiě)入數(shù)據(jù),也會(huì)導(dǎo)致阻塞针肥,直到channel中被寫(xiě)入數(shù)據(jù)為止
//=========
close(ch) // 關(guān)閉channel
有沒(méi)注意到關(guān)鍵字”阻塞“饼记?,這個(gè)其實(shí)是默認(rèn)的channel的接收和發(fā)送慰枕,其實(shí)也有非阻塞的具则,請(qǐng)看下文。
5.重要的四種通道使用
1.無(wú)緩沖通道
說(shuō)明:無(wú)緩沖通道上的發(fā)送操作將會(huì)被阻塞捺僻,直到另一個(gè)goroutine在對(duì)應(yīng)的通道上執(zhí)行接收操作乡洼,此時(shí)值才傳送完成崇裁,兩個(gè)goroutine都繼續(xù)執(zhí)行。
上代碼:
package main
import (
"fmt"
"time"
)
var done chan bool
func HelloWorld() {
fmt.Println("Hello world goroutine")
time.Sleep(1*time.Second)
done <- true
}
func main() {
done = make(chan bool) // 創(chuàng)建一個(gè)channel
go HelloWorld()
<-done
}
輸出:
Hello world goroutine
由于main不會(huì)等goroutine執(zhí)行結(jié)束才返回束昵,前文專(zhuān)門(mén)加了sleep輸出為了可以看到goroutine的輸出內(nèi)容拔稳,那么在這里由于是阻塞的,所以無(wú)需sleep锹雏。
(小嘗試:可以將代碼中”done <- true”和”<-done”巴比,去掉再執(zhí)行,看看會(huì)發(fā)生啥礁遵?)
2.管道
通道可以用來(lái)連接goroutine轻绞,這樣一個(gè)的輸出是另一個(gè)輸入。這就叫做管道佣耐。
例子:
package main
import (
"fmt"
"time"
)
var echo chan string
var receive chan string
// 定義goroutine 1
func Echo() {
time.Sleep(1*time.Second)
echo <- "咖啡色的羊駝"
}
// 定義goroutine 2
func Receive() {
temp := <- echo // 阻塞等待echo的通道的返回
receive <- temp
}
func main() {
echo = make(chan string)
receive = make(chan string)
go Echo()
go Receive()
getStr := <-receive // 接收goroutine 2的返回
fmt.Println(getStr)
}
在這里不一定要去關(guān)閉channel政勃,因?yàn)榈讓拥睦厥諜C(jī)制會(huì)根據(jù)它是否可以訪問(wèn)來(lái)決定是否自動(dòng)回收它。(這里不是根據(jù)channel是否關(guān)閉來(lái)決定的)
3.單向通道類(lèi)型
當(dāng)程序則夠復(fù)雜的時(shí)候兼砖,為了代碼可讀性更高奸远,拆分成一個(gè)一個(gè)的小函數(shù)是需要的。
此時(shí)go提供了單向通道的類(lèi)型讽挟,來(lái)實(shí)現(xiàn)函數(shù)之間channel的傳遞懒叛。
上代碼:
package main
import (
"fmt"
"time"
)
// 定義goroutine 1
func Echo(out chan<- string) { // 定義輸出通道類(lèi)型
time.Sleep(1*time.Second)
out <- "咖啡色的羊駝"
close(out)
}
// 定義goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定義輸出通道類(lèi)型和輸入類(lèi)型
temp := <-in // 阻塞等待echo的通道的返回
out <- temp
close(out)
}
func main() {
echo := make(chan string)
receive := make(chan string)
go Echo(echo)
go Receive(receive, echo)
getStr := <-receive // 接收goroutine 2的返回
fmt.Println(getStr)
}
程序輸出:
咖啡色的羊駝
4.緩沖管道
goroutine的通道默認(rèn)是是阻塞的,那么有什么辦法可以緩解阻塞耽梅?
答案是:加一個(gè)緩沖區(qū)薛窥。
對(duì)于go來(lái)說(shuō)創(chuàng)建一個(gè)緩沖通道很簡(jiǎn)單:
ch := make(chan string, 3) // 創(chuàng)建了緩沖區(qū)為3的通道
//=========
len(ch) // 長(zhǎng)度計(jì)算
cap(ch) // 容量計(jì)算
6.goroutine死鎖與友好退出
6.1 goroutine死鎖
來(lái)一個(gè)死鎖現(xiàn)場(chǎng)一:
package main
func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine, 通道被鎖
}
輸出:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
死鎖現(xiàn)場(chǎng)2:
package main
func main() {
cha, chb := make(chan int), make(chan int)
go func() {
cha <- 1 // cha通道的數(shù)據(jù)沒(méi)有被其他goroutine讀取走,堵塞當(dāng)前goroutine
chb <- 0
}()
<- chb // chb 等待數(shù)據(jù)的寫(xiě)
}
為什么會(huì)有死鎖的產(chǎn)生眼姐?
非緩沖通道上如果發(fā)生了流入無(wú)流出诅迷,或者流出無(wú)流入,就會(huì)引起死鎖妥凳。
或者這么說(shuō):goroutine的非緩沖通道里頭一定要一進(jìn)一出竟贯,成對(duì)出現(xiàn)才行。
上面例子屬于:一:流出無(wú)流入逝钥;二:流入無(wú)流出
當(dāng)然屑那,有一個(gè)例外:
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
}
執(zhí)行以上代碼將會(huì)發(fā)現(xiàn),竟然沒(méi)有報(bào)錯(cuò)艘款。
what持际?
不是說(shuō)好的一進(jìn)一出就死鎖嗎?
仔細(xì)研究會(huì)發(fā)現(xiàn)哗咆,其實(shí)根本沒(méi)等goroutine執(zhí)行完蜘欲,main函數(shù)自己先跑完了,所以就沒(méi)有數(shù)據(jù)流入主的goroutine晌柬,就不會(huì)被阻塞和報(bào)錯(cuò)
6.2 goroutine的死鎖處理
有兩種辦法可以解決:
1.把沒(méi)取走的取走便是
package main
func main() {
cha, chb := make(chan int), make(chan int)
go func() {
cha <- 1 // cha通道的數(shù)據(jù)沒(méi)有被其他goroutine讀取走姥份,堵塞當(dāng)前goroutine
chb <- 0
}()
<- cha // 取走便是
<- chb // chb 等待數(shù)據(jù)的寫(xiě)
}
2.創(chuàng)建緩沖通道
package main
func main() {
cha, chb := make(chan int, 3), make(chan int)
go func() {
cha <- 1 // cha通道的數(shù)據(jù)沒(méi)有被其他goroutine讀取走郭脂,堵塞當(dāng)前goroutine
chb <- 0
}()
<- chb // chb 等待數(shù)據(jù)的寫(xiě)
}
這樣的話(huà),cha可以緩存一個(gè)數(shù)據(jù)澈歉,cha就不會(huì)掛起當(dāng)前的goroutine了展鸡。除非再放兩個(gè)進(jìn)去,塞滿(mǎn)緩沖通道就會(huì)了埃难。
7.select的簡(jiǎn)介
定義:在golang里頭select的功能與epoll(nginx)/poll/select的功能類(lèi)似莹弊,都是堅(jiān)挺IO操作,當(dāng)IO操作發(fā)生的時(shí)候涡尘,觸發(fā)相應(yīng)的動(dòng)作忍弛。
select有幾個(gè)重要的點(diǎn)要強(qiáng)調(diào):
1.如果有多個(gè)case都可以運(yùn)行,select會(huì)隨機(jī)公平地選出一個(gè)執(zhí)行考抄,其他不會(huì)執(zhí)行
上代碼:
package main
import "fmt"
func main() {
ch := make (chan int, 1)
ch<-1
select {
case <-ch:
fmt.Println("咖啡色的羊駝")
case <-ch:
fmt.Println("黃色的羊駝")
}
}
輸出:
(隨機(jī))二者其一
1
2.case后面必須是channel操作细疚,否則報(bào)錯(cuò)。
上代碼:
package main
import "fmt"
func main() {
ch := make (chan int, 1)
ch<-1
select {
case <-ch:
fmt.Println("咖啡色的羊駝")
case 2:
fmt.Println("黃色的羊駝")
}
}
輸出報(bào)錯(cuò):
2 evaluated but not used
select case must be receive, send or assign recv
3.select中的default子句總是可運(yùn)行的座泳。所以沒(méi)有default的select才會(huì)阻塞等待事件
上代碼:
package main
import "fmt"
func main() {
ch := make (chan int, 1)
// ch<-1 <= 注意這里備注了惠昔。
select {
case <-ch:
fmt.Println("咖啡色的羊駝")
default:
fmt.Println("黃色的羊駝")
}
}
輸出:
黃色的羊駝
4.沒(méi)有運(yùn)行的case幕与,那么江湖阻塞事件發(fā)生報(bào)錯(cuò)(死鎖)
package main
import "fmt"
func main() {
ch := make (chan int, 1)
// ch<-1 <= 注意這里備注了挑势。
select {
case <-ch:
fmt.Println("咖啡色的羊駝")
}
}
輸出報(bào)錯(cuò):
fatal error: all goroutines are asleep - deadlock!
8.select的應(yīng)用場(chǎng)景
1.timeout 機(jī)制(超時(shí)判斷)
package main
import (
"fmt"
"time"
)
func main() {
timeout := make (chan bool, 1)
go func() {
time.Sleep(1*time.Second) // 休眠1s,如果超過(guò)1s還沒(méi)I操作則認(rèn)為超時(shí)啦鸣,通知select已經(jīng)超時(shí)啦~
timeout <- true
}()
ch := make (chan int)
select {
case <- ch:
case <- timeout:
fmt.Println("超時(shí)啦!")
}
}
以上是入門(mén)版潮饱,通常代碼中是這么寫(xiě)的:
package main
import (
"fmt"
"time"
)
func main() {
ch := make (chan int)
select {
case <-ch:
case <-time.After(time.Second * 1): // 利用time來(lái)實(shí)現(xiàn),After代表多少時(shí)間后執(zhí)行輸出東西
fmt.Println("超時(shí)啦!")
}
}
2.判斷channel是否阻塞(或者說(shuō)channel是否已經(jīng)滿(mǎn)了)
package main
import (
"fmt"
)
func main() {
ch := make (chan int, 1) // 注意這里給的容量是1
ch <- 1
select {
case ch <- 2:
default:
fmt.Println("通道channel已經(jīng)滿(mǎn)啦诫给,塞不下東西了!")
}
}
3.退出機(jī)制
package main
import (
"fmt"
"time"
)
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()
go func() {
DONE:
for {
time.Sleep(1*time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
break DONE // 跳出 select 和 for 循環(huán)
default:
}
}
}()
time.Sleep(time.Second * 4)
ch<-"stop"
}
輸出:
1532390471
1532390472
1532390473
stop
1532390474
這邊要強(qiáng)調(diào)一點(diǎn):退出循環(huán)一定要用break + 具體的標(biāo)記香拉,或者goto也可以。否則其實(shí)不是真的退出中狂。
package main
import (
"fmt"
"time"
)
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()
go func() {
for {
time.Sleep(1*time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
goto DONE // 跳出 select 和 for 循環(huán)
default:
}
}
DONE:
}()
time.Sleep(time.Second * 4)
ch<-"stop"
}
輸出:
1532390525
1532390526
1532390527
1532390528
stop
9.select死鎖
select不注意也會(huì)發(fā)生死鎖凫碌,前文有提到一個(gè),這里分幾種情況胃榕,重點(diǎn)再次強(qiáng)調(diào):
1.如果沒(méi)有數(shù)據(jù)需要發(fā)送盛险,select中又存在接收通道數(shù)據(jù)的語(yǔ)句,那么將發(fā)送死鎖
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
預(yù)防的話(huà)加default勋又。
空select苦掘,也會(huì)引起死鎖
package main
func main() {
select {}
}
版權(quán)聲明:本文為CSDN博主「咖啡色的羊駝」的原創(chuàng)文章,遵循CC 4.0 by-sa版權(quán)協(xié)議楔壤,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明鹤啡。
原文鏈接:https://blog.csdn.net/u011957758/article/details/81159481