初識golang-數據類型_channel

查看上一篇結構體類型請點

一.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的介紹基本結束,以後有別的用法新功能再來更來更新。

查看下一篇函數類型請點

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章