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