什么是并發(fā)
代碼按照順序執(zhí)行,即執(zhí)行完一句才會(huì)執(zhí)行下一句按价,這樣的代碼邏輯簡(jiǎn)單,符合人類的閱讀習(xí)慣。但是這樣是不夠的属韧,因?yàn)橛?jì)算機(jī)非常強(qiáng)大,比如一款音樂軟件蛤吓,在聽音樂的時(shí)候想下載歌曲宵喂,**同一時(shí)刻做兩件事,在編程中会傲,這就是并發(fā)**锅棕,并發(fā)可以讓編寫的程序在同一時(shí)刻做幾件事拙泽。
進(jìn)程和線程
講并發(fā)就繞不開線程,不過在介紹線程前裸燎,先介紹什么是進(jìn)程顾瞻。
**進(jìn)程**
在操作系統(tǒng)中,進(jìn)程是一個(gè)非常重要的概念德绿。當(dāng)啟動(dòng)一個(gè)軟件時(shí)荷荤,操作系統(tǒng)會(huì)為這個(gè)軟件創(chuàng)建一個(gè)進(jìn)程,這個(gè)進(jìn)程就是軟件的工作空間移稳,它包含了軟件運(yùn)行所需的所有資源蕴纳,比如內(nèi)存空間、文件句柄个粱,還有線程古毛。
image-20211213214435023
**線程**
線程是進(jìn)程對(duì)的執(zhí)行空間,一個(gè)進(jìn)程可以有多個(gè)線程几蜻,線程被操作系統(tǒng)調(diào)度執(zhí)行喇潘,比如下載一個(gè)文件,發(fā)送一個(gè)消息等梭稚。這種**多個(gè)線程被調(diào)度系統(tǒng)同時(shí)調(diào)度執(zhí)行的情況颖低,就是多線程并發(fā)**。
一個(gè)程序啟動(dòng)弧烤,就會(huì)有對(duì)應(yīng)的進(jìn)程被創(chuàng)建忱屑,同時(shí)進(jìn)程也會(huì)啟動(dòng)一個(gè)線程,這個(gè)線程就叫主線程暇昂。如果主線程結(jié)束莺戒,那么整個(gè)流程就退出了。有了主線程急波,就可以從主線程里啟動(dòng)其他線程从铲,也就有了多線程并發(fā)。
協(xié)程(Goroutine)
Go語言中**沒有線程的概念澄暮,只有協(xié)程名段,也稱為goroutine**。相比線程來說泣懊,協(xié)程更加輕量伸辟,一個(gè)程序可以隨意啟動(dòng)成千上萬個(gè)goroutine。
goroutine被Go Runtime調(diào)度馍刮,這一點(diǎn)和線程不一樣信夫。也即是說,Go語言的并發(fā)是由Go自己所調(diào)度的,自己決定同事執(zhí)行多少個(gè)goroutine静稻,什么時(shí)候執(zhí)行哪幾個(gè)警没。這對(duì)于開發(fā)者來說是完全透明,只需要在編碼的時(shí)候告訴Go語言要啟動(dòng)幾個(gè)goroutine姊扔,至于如何調(diào)度執(zhí)行惠奸,不用關(guān)心。
要啟動(dòng)一個(gè)goroutine非常簡(jiǎn)單恰梢,Go語言提供了 **go 關(guān)鍵字**,相比其他編程語言簡(jiǎn)化了很多
func main() {
go fmt.Println("奔跑的蝸牛")
fm.Println("獨(dú)臂阿童木")
time.Sleep(time.Second)
}
// 輸出
獨(dú)臂阿童木
奔跑的蝸牛
// 程序是并發(fā)的梗掰,go關(guān)鍵字啟動(dòng)的goroutine并不阻塞main goroutine的執(zhí)行嵌言,所以才會(huì)打印出上述結(jié)果。示例中main goroutine等待一秒及穗,避免main goroutine執(zhí)行完就退出了摧茴,看不到啟動(dòng)的新goroutine打印結(jié)果
這樣就啟動(dòng)了一個(gè)goroutine,用來調(diào)度fmt.Println()函數(shù)埂陆。這段代碼里有兩個(gè)goroutine苛白,一個(gè)是main函數(shù)啟動(dòng)的main goroutine,一個(gè)是自己通過go關(guān)鍵字啟動(dòng)的goroutine。
**go 關(guān)鍵字** 緊跟一個(gè)方法或者函數(shù)焚虱,就可以啟動(dòng)一個(gè)goroutine购裙,讓方法在這啟動(dòng)的goroutine中運(yùn)行。
Channel
如果啟動(dòng)了多個(gè)goroutine鹃栽,他們之間如何通信呢躏率?這就是Go語言提供的 **Channel(通道)** 要解決的問題。
**聲明一個(gè) channel**
在Go語言中民鼓,聲明一個(gè)channel非常簡(jiǎn)單薇芝,使用內(nèi)置make函數(shù)即可
ch := make(chan string)
// chan 是關(guān)鍵字,表示是chan類型丰嘉,后面string表示channel里面的數(shù)據(jù)是string類型夯到。通過channel申明可以看到,chan是一個(gè)集合類型
定義好chan后就可以使用了饮亏,一個(gè)chan操作只有兩種:**發(fā)送和接收耍贾。**
接收: 獲取chan中的值,操作符為 <-chan
-
發(fā)送:向chan發(fā)送值克滴,把值放在chan中逼争,操作符為 chan <-
==小技巧==:注意發(fā)送和接收的操作符都是<-,只不過位置不同劝赔。接收的操作符在chan的左側(cè)誓焦,發(fā)送的操作符在chan的右側(cè)。
func main() {
ch := make(chan string)
go func() {
fmt.Println("奔跑的蝸牛")
ch <- "goroutine完成"
}()
fmt.Println("獨(dú)臂阿童木")
v := <-ch
fmt.Println("接收到的chan中的值:", v)
}
// 程序并沒有直接退出杂伟,可以看到打印結(jié)果移层,達(dá)到了time.Sleep函數(shù)的效果
// 因?yàn)橥ㄟ^maike創(chuàng)建的chan中沒有值,而main goroutine想從chan中獲得值赫粥,獲取不到就一直等待观话,等到另一個(gè)goroutine向chan中發(fā)送值為止
// 出書
獨(dú)臂阿童木
奔跑的蝸牛
接收到的chan中的值: goroutine完成
上述示例中,在新啟動(dòng)的goroutine中向chan類型的變量ch發(fā)送值越平;在main goroutine中频蛔,從變量ch接收值,如果ch中沒有值秦叛,則阻塞等待ch中有值可以接收為止晦溪。
channel有點(diǎn)像在兩個(gè)goroutine之間架起管道,一個(gè)goroutine可以往這個(gè)管道發(fā)送數(shù)據(jù)挣跋,另一個(gè)可以從這個(gè)管道接收數(shù)據(jù)三圆,有點(diǎn)類似隊(duì)列。
**無緩沖channel**
上述示例中make創(chuàng)建的就是一個(gè)無緩沖channel避咆,它的容量是0舟肉,不能存儲(chǔ)任何數(shù)據(jù)。所以無緩沖channel只起傳輸數(shù)據(jù)的作用查库,數(shù)據(jù)并不會(huì)在channel中停留路媚。這也意味著,**無緩沖channel的發(fā)送和接收操作是痛失進(jìn)行的**膨报,它也被稱為**同步channel**磷籍。
**有緩沖channel**
有緩沖channel類似一個(gè)阻塞對(duì)壘,內(nèi)部的元素先進(jìn)先出现柠。通過**make函數(shù)的第二個(gè)參數(shù)可以指定channel容量的大小院领,進(jìn)而創(chuàng)建一個(gè)有緩沖channel**
cacheCh := make(chan string, 3)
// 創(chuàng)建一個(gè)容量為5的channel,內(nèi)部元素類型是string够吩,也就是說這個(gè)channel內(nèi)部最多可以存放5個(gè)類型為string的元素
一個(gè)有緩沖channel具備一下特點(diǎn):
1. 有緩沖channel的內(nèi)部有一個(gè)緩沖隊(duì)列
2. 發(fā)送操作是向?qū)镜奈膊坎迦朐乇热唬绻?duì)列已滿,則阻塞等待周循,直到另一個(gè)goroutine執(zhí)行强法,接收操作釋放隊(duì)列空間
3. 接收操作是從隊(duì)列的頭部獲取元素并把它從隊(duì)列中刪除,如果隊(duì)列為空湾笛,則阻塞等待饮怯,知道另一個(gè)goroutine執(zhí)行,發(fā)送數(shù)據(jù)插入新的元素
因?yàn)橛芯彌_channel類似一個(gè)隊(duì)列嚎研,可以獲取它的容量和里面元素的個(gè)數(shù)蓖墅。
cacheCh := make(chan int, 5)
cachech <- 2
cachech <- 4
fmt.Println("cacheCh 容量為:", cap(cacheCh), "元素個(gè)數(shù)為:", len(cacheCh))
// 通過內(nèi)置函數(shù)cap可以獲取channel的容量,也就是最大能存放多少元素论矾,通過內(nèi)置函數(shù)len可以獲取channel中元素的個(gè)數(shù)
==小提示==:無緩沖channel其實(shí)就是一個(gè)容量大小為0的channel教翩。比如 **make(chan int, 0)**
**關(guān)閉 channel**
channel 可以使用內(nèi)置函數(shù) close 關(guān)閉。
close(cacheCh)
如果一個(gè)channel被關(guān)閉了贪壳,就不能向里面發(fā)送數(shù)據(jù)了饱亿,如果發(fā)送的話,會(huì)引起panic異常闰靴。但是還可以接收channel里的數(shù)據(jù)彪笼,如果channel里面沒有數(shù)據(jù)的話,接收的數(shù)據(jù)是元素類型的零值蚂且。
**單向channel**
有時(shí)候杰扫,因?yàn)樘厥獾臉I(yè)務(wù)需求,比如**限制一個(gè)channel只可以接收但不能發(fā)送膘掰,或者限制一個(gè)channel只能發(fā)送但不能接接收,這種channel被稱為單向channel**佳遣。
單向channel的聲明也很簡(jiǎn)單识埋,只需要在聲明的時(shí)候帶上<- 操作符即可。
onlySend := make(chan <- int)
onlyReceive := make(<-chan int)
// 注意: 聲明單向channel <- 操作符的位置和上面發(fā)送接收操作是一樣的零渐。
在函數(shù)后者方法的參數(shù)中窒舟,使用單向channel比較多,這樣可以防止一些操作影響了channel.
func counter(out chan <- int) {
// 函數(shù)內(nèi)容使用變量out诵盼, 只能進(jìn)行發(fā)送操作
}
**select + channel示例**
假如我們要下載一個(gè)文件惠豺,啟動(dòng)了3個(gè)goroutine進(jìn)行下載,并發(fā)結(jié)果發(fā)送到3個(gè)channel中风宁。哪個(gè)先下載好洁墙,就先用哪個(gè)channel的結(jié)果。
在Go語言中戒财,通難過select語句實(shí)現(xiàn)多路復(fù)用操作热监,語句格式如下:
select {
case i1 <- c1 :
// todo
case i2 <- c2 :
// todo
default :
// todo
}
整體結(jié)構(gòu)和switch很像,都有case和default饮寞,只不過select和case是一個(gè)個(gè)可以操作的channel孝扛。
==小提示==:多路復(fù)用可以簡(jiǎn)單的理解為,N個(gè)channel中幽崩,任意一個(gè)channel有數(shù)據(jù)產(chǎn)生苦始,select都可以監(jiān)聽到, 然后執(zhí)行對(duì)應(yīng)的分支慌申,接收數(shù)據(jù)并處理陌选。
// 多路下載示例
func main() {
// 聲明3個(gè)存放結(jié)果的channel
firstCh := make(chan string)
secondCh := make(chan string)
threeCh := make(chan string)
// 同時(shí)開啟3個(gè)goroutine下載
go func() {
firstCh <- downloadFile("firstCh")
}()
go func() {
secondCh <- downloadFile("secondCh")
}()
go func() {
threeCh <- downloadFile("threeCh")
}()
// 開啟多路復(fù)用,哪個(gè)channel能獲取到值,就先用哪個(gè)
select {
case filePath := <- firstCh :
fmt.Println(filePath)
case filePath := <- secoudCh :
fmt.Println(filePath)
case filePath := <- threeCh :
fmt.Println(filePath)
}
func downloadFile(chanName string) string {
// 模擬下載文件柠贤,可以隨機(jī)time.Sleep時(shí)間
time.Sleep(time.Second)
return chanName + "filePath"
}
}
如果這些case中有一個(gè)可以執(zhí)行香浩,select語句會(huì)選擇該case語句執(zhí)行,如果同時(shí)又多個(gè)case可以被執(zhí)行臼勉,則隨機(jī)選擇一個(gè)邻吭,這樣每個(gè)case都有平等的被執(zhí)行的機(jī)會(huì)。如果一個(gè)select沒有任何case宴霸,那么就會(huì)一直等下去囱晴。
小結(jié):channel是Go語言并發(fā)的基礎(chǔ),在Go語言中瓢谢,提倡通過通信來共享內(nèi)存畸写,而不是通過共享內(nèi)存來通信,其實(shí)就是提倡通過channel發(fā)送接收消息的方式進(jìn)行數(shù)據(jù)通信氓扛,而不是通過修改同一個(gè)變量枯芬。所以在數(shù)據(jù)流動(dòng)、傳遞的場(chǎng)景中要優(yōu)先使用channel采郎,它是并發(fā)安全的千所,性能也不錯(cuò)。