查看上一篇結構體類型請點我
一.goroutine
說到channel,就不得不提golang的goroutine,這是golang原生支持高併發很重的一點。併發模型有5種:
1.單進(線)程·循環處理請求
單進程和單線程其實沒有區別,因爲一個進程至少有一個線程。循環處理請求應該是最初級的做法。當大量請求進來時,單線程一個一個處理請求,請求很容易就積壓起來,得不到響應。這是無併發的做法。
2.多進程
主進程監聽和管理連接,當有客戶請求的時候,fork 一個子進程來處理連接,父進程繼續等待其他客戶的請求。但是進程佔用服務器資源是比較多的,服務器負載會很高。這種架構的最大的好處是隔離性,子進程萬一 crash 並不會影響到父進程。缺點就是對系統的負擔過重。
3.多線程
和多進程的方式類似,只不過是替換成線程。主線程負責監聽、accept()連接,子線程(工作線程)負責處理業務邏輯和流的讀取。子線程阻塞,同一進程內的其他線程不會被阻塞。缺點是會頻繁地創建、銷燬線程,這對系統也是個不小的開銷。這個問題可以用線程池來解決。線程池是預先創建一部分線程,由線程池管理器來負責調度線程,達到線程複用的效果,避免了反覆創建線程帶來的性能開銷,節省了系統的資源。同時還需要處理同步的問題,當多個線程請求同一個資源時,需要用鎖之類的手段來保證線程安全。同步處理不好會影響數據的安全性,也會拉低性能。最重要的一點,一個線程的崩潰會導致整個進程的崩潰。
4.單線程·回調(callback)和事件輪詢
主進程(master 進程)首先通過 socket() 來創建一個 sock 文件描述符用來監聽,然後fork生成子進程(workers 進程),子進程將繼承父進程的 sockfd(socket 文件描述符),之後子進程 accept() 後將創建已連接描述符(connected descriptor)),然後通過已連接描述符來與客戶端通信。採用此種方式最經典的就是Nginx。
5.協程
協程基於用戶空間的調度器,具體的調度算法由具體的編譯器和開發者實現,相比多線程和事件回調的方式,更加靈活可控。不同語言協程的調度方式也不一樣,python是在代碼裏顯式地yield進行切換,golang 則是用go語法來開啓 goroutine,具體的調度由語言層面提供的運行時執行。
gorounte 的堆棧比較小,一般是幾k,可以動態增長。線程的堆棧空間在 Windows 下默認 2M,Linux 下默認 8M。這也是goroutine 單機支持上萬併發的原因,因爲它更廉價。
從堆棧的角度,進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系統調度。線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度(內核線程)。協程和線程一樣共享堆,不共享棧,協程由程序員在協程的代碼裏顯示調度。
在使用 goroutine 的時候,可以把它當作輕量級的線程來用,和多進程、多線程方式一樣,主 goroutine 監聽,開啓多個工作 goroutine 處理連接。比起多線程的方式,優勢在於能開更多的 goroutine,來處理連接。
goroutine 的底層實現,關鍵在於三個基本對象上,G(goroutine),M(machine),P (process)。M:與內核線程連接,代表內核線程;P:代表M運行G所需要的資源,可以把它看做一個局部的調度器,維護着一個goroutine隊列;G:代表一個goroutine,有自己的棧。M 和 G 的映射,可以類比操作系統內核線程與用戶線程的 m:n 模型。通過對 P 數量的控制,可以控制操作系統的併發度。
到此,可以解釋golang爲何能原生支持高併發了。
接下來看看golang的goroutine怎麼用。golang是通過關鍵字go啓動goroutine,廢話少說,上例子。
package main
import (
"fmt"
"time"
)
func cal(a int , b int ) {
c := a+b
fmt.Printf("%d + %d = %d\n",a,b,c)
}
func main() {
for i :=0 ; i<10 ;i++{
go cal(i,i+1) //啓動10個goroutine 來計算
}
time.Sleep(time.Second * 2) // sleep作用是爲了等待所有任務完成
}
運行結果:
4 + 5 = 9
7 + 8 = 15
5 + 6 = 11
6 + 7 = 13
1 + 2 = 3
8 + 9 = 17
9 + 10 = 19
2 + 3 = 5
0 + 1 = 1
3 + 4 = 7
由於goroutine是異步執行的,那很有可能出現主程序退出時還有goroutine沒有執行完,此時goroutine也會跟着退出。此時如果想等到所有goroutine任務執行完畢才退出,go提供了sync包和channel來解決同步問題,當然如果你能預測每個goroutine執行的時間,你還可以通過time.Sleep方式等待所有的groutine執行完成以後在退出程序(如上面的列子)。
通過channel能在多個groutine之間通訊,當一個goroutine完成時候向channel發送退出信號,等所有goroutine退出時候,利用for循環channe去channel中的信號,若取不到數據會阻塞原理,等待所有goroutine執行完畢,使用該方法有個前提是你已經知道了你啓動了多少個goroutine,舉個例子。
package main
import (
"fmt"
"time"
)
func cal(a int , b int ,Exitchan chan bool) {
c := a+b
fmt.Printf("%d + %d = %d\n",a,b,c)
time.Sleep(time.Second*2)
Exitchan <- true
}
func main() {
Exitchan := make(chan bool,10) //聲明並分配管道內存
for i :=0 ; i<10 ;i++{
go cal(i,i+1,Exitchan)
}
for j :=0; j<10; j++{
<- Exitchan //取信號數據,如果取不到則會阻塞
}
close(Exitchan) // 關閉管道
}
運行結果:
9 + 10 = 19
3 + 4 = 7
6 + 7 = 13
5 + 6 = 11
4 + 5 = 9
2 + 3 = 5
0 + 1 = 1
1 + 2 = 3
7 + 8 = 15
8 + 9 = 17
goroutine之間可以通過channel進行通信或者說是數據共享,當然你也可以使用全局變量來進行數據共享。
package main
import (
"fmt"
"sync"
)
func Productor(mychan chan int,data int,wait *sync.WaitGroup) {
mychan <- data
fmt.Println("product data:",data)
wait.Done()
}
func Consumer(mychan chan int,wait *sync.WaitGroup) {
a := <- mychan
fmt.Println("consumer data:",a)
wait.Done()
}
func main() {
datachan := make(chan int, 100) //通訊數據管道
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go Productor(datachan, i,&wg) //生產數據
wg.Add(1)
}
for j := 0; j < 10; j++ {
go Consumer(datachan,&wg) //消費數據
wg.Add(1)
}
wg.Wait()
}
運行結果:
product data: 1
consumer data: 1
consumer data: 0
product data: 2
consumer data: 2
product data: 3
product data: 4
product data: 5
consumer data: 3
product data: 6
consumer data: 4
product data: 7
consumer data: 5
product data: 8
consumer data: 6
consumer data: 7
consumer data: 8
product data: 9
consumer data: 9
product data: 0
二.channel
說了這麼多,終於要說channel,估計很多同學沒看到這已經放棄了,爲什麼介紹前邊那麼多,因爲在golang中channel用的最多就是groutine之間的通信。channel俗稱管道,用於數據傳遞或數據共享,其本質是一個先進先出的隊列,使用goroutine+channel進行數據通訊簡單高效,同時也線程安全,多個goroutine可同時修改一個channel,不需要加鎖。接下來講講channe的用法,channel分三類;
1.只讀channel:只能讀channel裏面數據,不可寫入
read_only := make (<-chan int)
2.只寫channel:只能寫數據,不可讀
write_only := make (chan<- int)
定義只讀,只寫channle沒有意義,一般用於參數傳遞,舉個例子:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go send(c)
go recv(c)
time.Sleep(3 * time.Second)
}
//只能向chan裏寫數據
func send(c chan<- int) {
for i := 0; i < 10; i++ {
c <- i
}
}
//只能取channel中的數據
func recv(c <-chan int) {
for i := range c {
fmt.Println(i)
}
}
運行結果:
0
1
2
3
4
5
6
7
8
9
main函數中調整send方法和recv方法的調用順序會導致異常。
3.一般channel:可讀可寫
read_write := make (chan int, len)
channel在定義時可以指定長度,當定義時有寫紅色部分時爲有緩存channel,否則爲無緩存channel。
1)從無緩存的 channel 中讀取消息會阻塞,直到有 goroutine 向該 channel 中發送消息;同理,向無緩存的 channel 中發送消息也會阻塞,直到有 goroutine 從 channel 中讀取消息。使用方式如下:
package main
import (
"fmt"
)
func main() {
c := make(chan int)
//使用goroutine使當前channel的發送不會阻塞線程
go func() {
c <- 1
}()
fmt.Println(<-c)
}
運行結果:
1
2)有緩存的 channel 類似一個阻塞隊列(採用環形數組實現)。當緩存未滿時,向 channel 中發送消息時不會阻塞,當緩存滿時,發送操作將被阻塞,直到有其他 goroutine 從中讀取消息;相應的,當 channel 中消息不爲空時,讀取消息不會出現阻塞,當 channel 爲空時,讀取操作會造成阻塞,直到有 goroutine 向 channel 中寫入消息。使用方式如下:
package main
import (
"fmt"
)
func main() {
//創建一個緩衝大小爲2的channel
c := make(chan int, 2)
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
運行結果:
1
2
3)搭配range使用
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) //需要關閉,否則range遍歷的時候會異常
}()
for num := range ch {
fmt.Println("num = ", num)
}
}
運行結果:
num = 0
num = 1
num = 2
num = 3
num = 4
4)搭配select使用
select 可以同時監聽多個 channel 的消息狀態,golang 的 select 的功能和 select, poll, epoll
相似,就是監聽 IO 操作,當 IO 操作發生時,觸發相應的動作。舉個例子:
package main
import (
"fmt"
"time"
)
func main() {
timeout := make (chan bool, 1)
go func() {
time.Sleep(1e9 * 5) // sleep five second
timeout <- true
}()
ch := make (chan int)
//go func() {
// ch <- 1
//}()
select {
case <-ch:
fmt.Println("run ch")
case <-timeout:
fmt.Println("timeout!")
}
}
五秒後輸出結果:
timeout!
ps:
- select 可以同時監聽多個 channel 的寫入或讀取
- 執行 select 時,若只有一個 case 通過(不阻塞),則執行這個 case 塊
- 若有多個 case 通過,則隨機挑選一個 case 執行
- 若所有 case 均阻塞,且定義了 default 模塊,則執行 default 模塊。若未定義 default 模塊,則 select 語句阻塞,直到有 case 被喚醒。
- 使用 break 會跳出 select 塊。
5)超時時間
golang的time模塊中的time.after()返回值是chan類型,利用其可以實現timeout功能。舉個例子
package main
import (
"fmt"
"time"
)
func main() {
ch := make (chan int)
timeout := time.After(5 * time.Second)
select {
case <- ch:
fmt.Println("task finished.")
case <- timeout:
fmt.Println("task timeout.")
}
}
五秒後輸出結果:
task timeout.
6)quit信號
有一些場景中,一些 worker goroutine 需要一直循環處理信息,直到收到 quit 信號。
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c) //管道c中無數據會阻塞
}
quit <- 0
}()
fibonacci(c, quit)
}
運行結果:
0
1
1
2
3
5
8
13
21
34
quit
7)close
通過close函數來關閉channel。
package main
func main() {
c := make(chan int, 10)
c <- 1
c <- 2
close(c)
//c <- 3 //關閉後再插入會異常
}
到此,關於golang的channel的介紹基本結束,以後有別的用法新功能再來更來更新。
查看下一篇函數類型請點我