???個(gè)人主頁(yè):鼠鼠我捏,要死了捏的主頁(yè)?
???系列專欄:Golang全棧-專欄
???個(gè)人學(xué)習(xí)筆記,若有缺誤,歡迎評(píng)論區(qū)指正?
前些天發(fā)現(xiàn)了一個(gè)巨牛的人工智能學(xué)習(xí)網(wǎng)站,通俗易懂,風(fēng)趣幽默,忍不住分享一下給大家。點(diǎn)擊跳轉(zhuǎn)到網(wǎng)站AI學(xué)習(xí)網(wǎng)站。
前言
當(dāng)我們開發(fā)一個(gè)Web服務(wù)時(shí),我們希望可以同時(shí)處理成千上萬(wàn)的用戶請(qǐng)求,當(dāng)我們有大量數(shù)據(jù)要計(jì)算時(shí),我們希望可以同時(shí)開啟多個(gè)任務(wù)進(jìn)行處理,隨著硬件性能的提升以及應(yīng)用數(shù)據(jù)的增長(zhǎng),有越來(lái)越多的場(chǎng)景需要高并發(fā)處理,而高并發(fā)是Go的強(qiáng)項(xiàng)。
在這篇文章中,我們就一起來(lái)探究一下Go并發(fā)編程!
目錄
前言
并發(fā)與并行
并發(fā)
并行
Goroutines
什么是Goroutine
Goroutine的優(yōu)勢(shì)
啟動(dòng)Goroutine
關(guān)閉Goroutine
Channel
什么是Channel
創(chuàng)建Channel
Channel操作
發(fā)送與接收
關(guān)閉
遍歷
無(wú)緩沖區(qū)Channel
有緩沖區(qū)Channel
Channel的串聯(lián)
單方向的channel
select:多路復(fù)用
Goroutine泄漏
小結(jié)
并發(fā)與并行
在談Go并發(fā)編程之前,我們需要對(duì)并發(fā)與并行做一下區(qū)分。
并發(fā)
并發(fā)是指有多個(gè)任務(wù)處于運(yùn)行狀態(tài),但無(wú)法確定到底任務(wù)的運(yùn)行順序,比如某一時(shí)間,有一個(gè)雙核CPU,但有10個(gè)任務(wù)(線程),這些任務(wù)可能隨機(jī)被分配到相同或者不同的核心上去運(yùn)行,但是其運(yùn)行順序是不確定的。
并行
并行是指多個(gè)任務(wù)在某一個(gè)時(shí)刻同時(shí)運(yùn)行,比如某一個(gè)時(shí)刻,一個(gè)雙核心的CPU,兩個(gè)核心同時(shí)都有一個(gè)任務(wù)在運(yùn)行,那么就是說(shuō)這兩個(gè)任務(wù)是并行的。
Goroutines
Goroutine是 Go語(yǔ)言的并發(fā)單元。
什么是Goroutine
Goroutine,中文稱為協(xié)程,我們可以把 Goroutine看作是一個(gè)輕量級(jí)的線程,而從代碼層面來(lái)看,Goroutine就是一個(gè)獨(dú)立運(yùn)行的函數(shù)或方法。
Goroutine的優(yōu)勢(shì)
- 與線程相比,創(chuàng)建一個(gè)Goroutine的開銷要小得多,一個(gè)Goroutine初始化時(shí)只需要2KB,而一個(gè)線程則要2MB,所以Go程序可以大量創(chuàng)建Goroutine進(jìn)行并發(fā)處理。
- 雖然協(xié)程初始化只有2KB,但卻可以根據(jù)需求動(dòng)態(tài)擴(kuò)展。
- Goroutine可以通過(guò)Channel互相通訊,而線程只能通過(guò)共享內(nèi)存互相通訊。
- Goroutine由Go調(diào)度器進(jìn)行調(diào)度,而線程則依賴系統(tǒng)的調(diào)度。
啟動(dòng)Goroutine
要啟動(dòng)一個(gè)Goroutine非常簡(jiǎn)單,只要在函數(shù)或者方法前面加上 go關(guān)鍵字就可以了:
package main
func Hello(){
fmt.Println("hello")
}
func main(){
go Hello()
//匿名函數(shù)
go func(){
fmt.Println("My Goroutine")
}()
}
程序啟動(dòng)后, main函數(shù)單獨(dú)運(yùn)行在一個(gè) Goroutine中,這個(gè) Goroutine稱作 Main Goroutine,其他用go關(guān)鍵字啟動(dòng)的Goroutine各自運(yùn)行。
如果你在控制臺(tái)運(yùn)行上面的程序,會(huì)發(fā)現(xiàn)在控制臺(tái)根據(jù)沒有任何輸出,這是為什么呢?
原因在于雖然所有的Goroutine是獨(dú)自運(yùn)行的,但如果 Man Gorouine終止的話,那么所有 Goroutine 都會(huì)退出執(zhí)行。
上面的示例中,我們啟動(dòng)的 Goroutine還沒運(yùn)行,main函數(shù)就執(zhí)行結(jié)束了,因此整個(gè)程序就退出了。
package main
import "time"
func Hello(){
fmt.Println("hello")
}
func main(){
go Hello()
go func(){
fmt.Println("My Goroutine")
}()
time.Sleep(time.Second)
}
上面的示例中,我們調(diào)用 time.Sleep()函數(shù)讓 Main Goroutine休眠而不退出,這時(shí)候其他的Goroutine就可以在 Main Goroutine退出前執(zhí)行。
關(guān)閉Goroutine
Go沒有提供關(guān)閉Goroutine的機(jī)制,一般來(lái)說(shuō)要讓一個(gè)Goroutine停止有三種方式:
- random?Goroutine執(zhí)行完成退出或者 return退出
- main函數(shù)執(zhí)行完成,所有Goroutine自然就會(huì)終止
- 直接終止整個(gè)程序的執(zhí)行(程序崩潰或調(diào)用os.Exit()),類似第2種方式。
Channel
Go并發(fā)編程的思想是:不要用共享內(nèi)存來(lái)通訊,而是用通訊來(lái)共享內(nèi)存。而這種通訊機(jī)制就是Channel。
什么是Channel
Channel是 Goroutine之間的通信機(jī)制,可以把 Channel理解為 Goroutine之間的一條管道,就像水可以從一個(gè)管道的一端流向另一端一樣,數(shù)據(jù)也可以通過(guò) Channel從一個(gè) Goroutine流向其他的一個(gè) Goroutine,以實(shí)現(xiàn) Goroutine之間的數(shù)據(jù)通訊。
創(chuàng)建Channel
創(chuàng)建 Channel類型的關(guān)鍵字是 chan,在 chan后面跟一個(gè)其他的數(shù)據(jù)類型,用于表示該 channel可發(fā)送什么類型的數(shù)據(jù),比如一個(gè)可以發(fā)送整數(shù)的 Channel其定義是:
var ch chan int
Channel的默認(rèn)值為nil,Channel必須實(shí)例化后才能使用,使用 make()函數(shù)實(shí)例化:
ch = make(chan int)
ch1 := make(chan int)
Channel與map一樣是引用數(shù)據(jù)類型,在調(diào)用make()函數(shù)后,該Channel變量引用一塊底層數(shù)據(jù)結(jié)構(gòu),因此當(dāng)把channel變量傳遞給函數(shù)時(shí),調(diào)用者與被調(diào)用者引用的是同一塊數(shù)據(jù)結(jié)構(gòu)。
Channel操作
Channel支持發(fā)送與接收兩種操作,無(wú)論是發(fā)送還是接收,都是用 <-運(yùn)算符。
發(fā)送與接收
向Channel發(fā)送數(shù)據(jù)時(shí),運(yùn)算符 <-放在channel變量的右邊,運(yùn)算符與Channel變量之間可以有空格:
ch <- x
接收Channel數(shù)據(jù)時(shí),運(yùn)算符 <-
放在channel變量的左邊且之間不能有空格:
x <-ch
x <- ch //錯(cuò)誤寫法
不接收channel的結(jié)果也是可以的:
<-ch
一個(gè)示例:
package main
import "fmt"
func main() {
ch := make(chan int)
go func(ch chan int) {
ch <- 10
}(ch)
m := <-ch
fmt.Println(m)
}
關(guān)閉
使用內(nèi)置 close可以關(guān)閉 Channel:
close(ch)
在關(guān)閉之后,如果再對(duì)該channel發(fā)送數(shù)據(jù)會(huì)導(dǎo)致panic錯(cuò)誤:
close(ch)
ch <- x //panic
如果Channel中還有值未被接收,在關(guān)閉之后,還可以接收Channel里的值,如果沒有值,則返回一個(gè)0值。
package main
import "fmt"
func main() {
ch := make(chan int)
go func(ch chan int) {
ch <- 10
close(ch) //關(guān)閉
}(ch)
m := <-ch
n := <-ch
//10,0
fmt.Println(m, n)
}
在從Channel接收值的時(shí)候,也可以多接收一個(gè)布爾值,如果為true,表示可以接收到有效值,如果沒有值,則表示Channel被關(guān)閉且沒有值:
n,ok := <-ch
關(guān)閉一個(gè)已經(jīng)關(guān)閉的Channel會(huì)導(dǎo)致panic,關(guān)閉一個(gè)nil值的Channel也會(huì)導(dǎo)致panic。
遍歷
Channel也可以用for...range語(yǔ)句來(lái)遍歷:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func(ch chan int) {
ch <- 10
ch <- 20
}(ch)
go func(ch chan int) {
for c := range ch {
fmt.Println(c)
}
}(ch)
time.Sleep(time.Second)
}
無(wú)緩沖區(qū)Channel
上面的示例中,調(diào)用make()函數(shù)時(shí)沒有指定第二個(gè)參數(shù),這時(shí)創(chuàng)建的Channel稱為無(wú)緩沖區(qū)Channel。
對(duì)于使用無(wú)緩沖區(qū)進(jìn)行通訊的兩個(gè)Goroutine來(lái)說(shuō),發(fā)送與阻塞都有可能會(huì)被阻塞,因此,本質(zhì)使用無(wú)緩沖區(qū)的channel進(jìn)行傳輸數(shù)據(jù)就是兩個(gè)Goroutine之間的一次數(shù)據(jù)同步,無(wú)緩沖區(qū)的Channel又被稱為同步Channel:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 10
}()
fmt.Println(<-ch)
}
有緩沖區(qū)Channel
調(diào)用 make()函數(shù)實(shí)例化 Channel時(shí),也可以通過(guò)該函數(shù)的第二個(gè)參數(shù)指定 Channel的容量:
ch := make(chan int,2)
通過(guò) cap()和 len()函數(shù)可以 Channel的長(zhǎng)度:
cap(ch) //2
len(ch) //0
ch <- 10
len(ch) //1
對(duì)于帶有緩沖區(qū)的Channel來(lái)說(shuō),當(dāng)Channel容量滿了,發(fā)送操作會(huì)阻塞,當(dāng)Channel空的時(shí)候,接收操作會(huì)阻塞,只有當(dāng)Channel未滿且有數(shù)據(jù)時(shí),發(fā)送與接收才不會(huì)發(fā)生阻塞。
Channel的串聯(lián)
Channel是Goroutine之間溝通的管道,日常生活中,管道可以連接在一起,水可以從一條管道流向另一條管道,而Channel也是一樣的,數(shù)據(jù)可以從一個(gè)Channel流向另一個(gè)Channel。
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for x := 0; x < 100; x++ {
ch1 <- x
}
close(ch1)
}()
go func() {
for {
x, ok := <-ch1
if !ok {
break
}
ch2 <- x * x
}
close(ch2)
}()
for x := range ch2 {
fmt.Println(x)
}
}
單方向的channel
利用Channel進(jìn)行通訊的大部分應(yīng)用場(chǎng)景是一個(gè)Goroutine作為生產(chǎn)者,只負(fù)責(zé)發(fā)送數(shù)據(jù),而另一個(gè)Goroutine作為消費(fèi)者,接收數(shù)據(jù)。
對(duì)于生產(chǎn)者來(lái)說(shuō),不會(huì)對(duì)Channel執(zhí)行接收的操作,對(duì)于消費(fèi)者來(lái)說(shuō)不會(huì)對(duì)Channel執(zhí)行發(fā)送的操作
在聲明Channel變量將<-運(yùn)算符放在 chan
關(guān)鍵前面則該Channel只能執(zhí)行接收操作:
//只允許接收
var ch1 <-chan int
在聲明Channel變量將<-運(yùn)算符放在 chan
關(guān)鍵字后面可以則該Channel只能執(zhí)行發(fā)送操作:
//只允許發(fā)送
var ch2 chan<- int
像我們前面那正常聲明一個(gè)Channel變量,則允許對(duì)該Channel執(zhí)行發(fā)送和接收操作:
//可以發(fā)送和接收
var ch3 chan int
從一個(gè)只能發(fā)送數(shù)據(jù)的channel接收數(shù)據(jù)無(wú)法通過(guò)編譯:
var ch chan<- int
x := <-ch //報(bào)錯(cuò)
向一個(gè)只有接收數(shù)據(jù)的channel發(fā)送數(shù)據(jù)無(wú)法通過(guò)編譯:
var ch <-chan int
ch <- 10 //報(bào)錯(cuò)
對(duì)一個(gè)只有接收操作的 Channel執(zhí)行 close()也無(wú)法通過(guò)編譯:
var ch <-chan int
close(ch) //報(bào)錯(cuò)
select:多路復(fù)用
前面的示例中,我們?cè)谝粋€(gè) Goroutine中只向一個(gè) Channel發(fā)送數(shù)據(jù)或者只從一個(gè) Channel接收數(shù)據(jù),因?yàn)槿绻瑫r(shí)向兩個(gè)Channel接收或發(fā)送數(shù)據(jù)時(shí),如果第一個(gè)Channel沒有事件響應(yīng),程序會(huì)一直阻塞:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func(ch1 chan int, ch2 chan int) {
fmt.Println("向ch1發(fā)送數(shù)據(jù)前")
<-ch1
fmt.Println("從ch2接收數(shù)據(jù)前")
ch2 <- 1
}(ch1, ch2)
time.Sleep(1 * time.Second)
}
但很多場(chǎng)景下,我們需要在一個(gè)Goroutine中根據(jù)不同的Channel執(zhí)行不同的操作:比如一個(gè)啟動(dòng)的Web服務(wù)器,在一個(gè)Goroutine中一邊處理請(qǐng)求,一邊監(jiān)聽信號(hào)量。要怎么做呢?
答案是:使用select語(yǔ)句,即多路復(fù)用,select語(yǔ)法類似switch語(yǔ)句,select語(yǔ)句塊中可以包含多個(gè)case分支和一個(gè)default分支,每個(gè)case分支表示一個(gè)向Channel發(fā)送或接收的操作,select語(yǔ)句會(huì)選擇可以執(zhí)行的case分支來(lái)執(zhí)行,如果沒有,則執(zhí)行default分支:
select {
case <-ch1:
// do something
case x := <-ch2:
// do somthing with x
case ch3 <- y:
// do something
default:
// dosomthing
}
下面我們通過(guò)一個(gè)案例來(lái)了解如何使用select語(yǔ)句,在這個(gè)例子中,我們模擬啟動(dòng)一個(gè)Web服務(wù)器處理來(lái)自用戶的請(qǐng)求,而在處理請(qǐng)求的同時(shí),還要可以根據(jù)接收的信息及時(shí)停止服務(wù),我們?cè)陂_啟單獨(dú)的一個(gè)Goroutine模擬向我們的Web發(fā)送停止信號(hào):
package main
import (
"fmt"
"time"
)
func main() {
s := make(chan struct{})
go func(s chan struct{}) {
time.Sleep(time.Microsecond * 100)
s <- struct{}{}
}(s)
MyWebServer(s)
fmt.Println("服務(wù)已停止...")
}
func MyWebServer(stop chan struct{}) {
for {
select {
case <-stop:
fmt.Println("服務(wù)器接收到停止信號(hào)")
return
default:
}
//模擬處理請(qǐng)求
go HandleQuery()
}
}
func HandleQuery() {
fmt.Println("處理請(qǐng)求...")
}
Goroutine泄漏
一個(gè) Goroutine 由于從Channel接收或向 Channel 發(fā)送數(shù)據(jù)一直被阻塞,一直無(wú)法往下執(zhí)行時(shí),這種情況稱為 Goroutine泄漏:
package main
import "time"
func main() {
ch := make(chan int)
go func() {
ch <- 10
}()
time.Sleep(time.Second * 2)
}
Goroutine執(zhí)行完成退出后,由Go內(nèi)存回收機(jī)制進(jìn)行回收,但是發(fā)生內(nèi)存泄漏的Goroutine并不會(huì)被回收,因此要避免發(fā)生這種情況。
總結(jié)
Go在語(yǔ)言層面支持并發(fā)編程,只需要在函數(shù)或者方法前加上go關(guān)鍵字便可以啟動(dòng)一個(gè)Goroutine,而Channel作為Goroutine之間的通訊管道,可以非常方便Goroutine之間的數(shù)據(jù)通訊。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-830276.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-830276.html
到了這里,關(guān)于掌握Go并發(fā):Go語(yǔ)言并發(fā)編程深度解析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!