相關文章:
- chan基本知識
- go中關於for循環的知識
- 多進程通信
前言
學習go語言已經有幾個月,但是關於go中的特性chan和routine的應用還不是很理解,如果搞不懂chan和routine的機制就很難流暢的用go編寫出健壯的程序。所以我覺得關於chan應用的編程,是可以講一講的。
那我們首先會想到幾個問題:
- 使用chan的代碼與普通的代碼有什麼不同嗎?
- 使用了chan後有什麼好處嗎?
- 怎麼才能正確的使用chan呢?
我們先看第一個問題,衆所周知 在go中chan是用來多個協程之間進行通信的,chan的應用思維是一種類似與client/servier的思維。也就是要有信息的生產者和消費者。既然如此,那就會設計到 不通協程之間的通信,而在順序編程中是不會設計到通信的,是一種線性的流程。類似與下圖:
在圖中,我們看到,生產者和消費者之間的程序通過通信來進行相互影響的,而線性的程序是不會有這種問題,不管調用了多少函數,都只會在一條進程上順序進行。
在下文中我們會用到一箇中間者的概念。中間者是承接上游的消費者和開啓下游的生產者。
單中間者
單中間者,就是在程序中只有一個線程連接生產者和消費者,不會進行擴增。
如下圖所示:
在這種方式中,只有一箇中間者,或者沒有中間者。
- gen.go
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
分析:創建一個out的通道,並打開一條協程將nums通過循環傳遞給通道out,在所有的nums都傳入out後關閉通道。在打開協程後返回創建的out。
這個函數在程序中起着生產者的作用,生產者和消費這之間的交流通道就是這個out,當out中消費一個數後,本函數就會再將一個數推送到通道out中。
- sq.go
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
創建了通道out作爲生產者的信息輸出,並接受in作爲消費者的信息輸入。
這程序中,即是消費者也是生產者,並在中間進行了數據的處理nn。作爲消費者,當通道in中可以取數據的時候,取出n並進行運算nn,然後作爲生產者將結果推送到out中。當in中有值時會進行消費,並計算nn,當out爲空時會進行生產,將nn的結果推到out中。
- main.go
func main() {
in := gen(2, 3)
c1 := sq(in)
for n := range c1 {
fmt.Println(n) // 4 9
}
}
main函數先調用了上面兩個函數,然後用了一個循環進行輸出。
輸出這個循環就是整個程序的消費者,當c1中有值時,就會取出來放到n中。
單中間者並不能體現使用chan的優越性,整個過程仍然是類似於線性的流程進行。在下文中我們會開啓多箇中間者進行復雜的數據運算和處理,因爲我們可以同時處理多條數據,必然給我們帶來性能上的提高。
多中間者
單中間者,就是在程序中只有一個線程連接生產者和消費者,不會進行擴增。
在上圖中,一個生產者被多箇中間者處理生產的信息,以提高消費的效率。然後用一個merge的中間者收集各個中間者生產的信息,並將這些信息統一的交給消費者。
- merge.go
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// Start an output goroutine for each input channel in cs. output
// copies values from c to out until c is closed, then calls wg.Done.
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
// Start a goroutine to close out once all the output goroutines are
// done. This must start after the wg.Add call.
go func() {
time.Sleep(222 * time.Second)
wg.Wait()
close(out)
}()
return out
}
這個merge函數給我帶來了很大的困擾。
- 程序中有兩個for循環,其中對cs的循環是針對數組的,循環的作用是對每個通道打開一個協程。而協程中的循環是監控通道用的,當通道close時推出循環。
- 爲什麼wg.Wait要用一個新的協程來,如果不用新的協程而寫在函數體裏面不行嗎?當然是不行的,這裏要注意一個與普通程序的分別:在使用chan的程序中,主程序的作用類似與初始化service和client而不進行具體的運算,所以肯定不能在初始化的時候進行wait,因爲這裏還是要進行生產,如果進行wait就不會運行main中的print(也就是消費者),而通道內的數據不進行消費就會形成阻塞,導致程序無法運行。
*那我已經知道了不能用在函數體裏面,爲什麼我另起一條協程,不會出問題呢?? 這涉及到後面部分,如果你明白了程序運行的整個流程,肯定可以解決這個疑問,我們把這個問題留到後面解決
- main.go
func main() {
in := gen(2, 3)
// Distribute the sq work across three goroutines that both read from in.
c1 := sq(in)
c2 := sq(in)
// Consume the merged output from c1 and c2.
for n := range merge(c1, c2) {
fmt.Println(n) // 4 then 9, or 9 then 4
}
}
在函數中先用生產者,通過通道傳遞兩個數,然後用兩個sq中間者進行中間計算,然後用merge進行集合,最後通過print進行消費。
在初次看到這個程序的時候,我是覺得程序不會輸出所有的結果的看,爲什麼我會這麼看呢?因爲在merge中的wait是在另起的一個協程中運行的,而不是在主程序中。如果不能理清楚整個程序的流程就會第一時間出現這種誤解。那我們只有模擬程序一步一步的運行,來判斷爲什麼這個程序可以順利的運行。
流程圖
因此我畫了下面的流程圖,圖中標註了程序的step。(不通協程之間的step是可以重合的)
我們來過一遍上圖的整個流程,你就發現程序中的所有疑問都迎刃而解。(雖然我們這裏使用了多箇中間者的方式,但是因爲我們僅僅模擬了一個數據輸入,所以只展示一箇中間者就可以表示整個流程)
流程圖分析
step 0:
[main] 調用程序 gen(3)
step 1:
[gen] 定義通道 out
step 2:
step 3:
[gen] 進入協程sub(gen)中的循環,判斷循環是否結束,同時return out
step 4:
[main] 進入程序sq
[sq] 定義通道out,gen中的out定義爲in
[sub(gen)] n-> out ##因爲還沒有遇到消費者,所以造成了阻塞
step 5:
[sq]進入協程sub(sq)中的循環,判斷循環是否結束,同時return out
step 6:
[main] 進入程序merge
[merge] wg.Add(1)並判斷cs[]循環時候結束
[sub(sq)] 從in中取出數n,並計算n*n,然後傳入通道out ##這時in通道不阻塞 out 通道阻塞
step 7:
[sub(gen)] out不再阻塞,判斷循環是否結束
[merge] 判斷循環是否結束,如果沒結束,進入sub(merge),結束進入sub(wait)
step 8:
[sub(gen)] 判斷循環結束,close(out)
[sub(merge)]判斷傳入的通道是否關閉,
[sub(wait)] 判斷merge協程是否都已經運行結束,沒有結束,等待
[merge]return out 返回main
step 9:
[sub(merge)] 判斷通道c未關閉,將c->out, ##這裏out中的值沒有消費,阻塞。c被消費不在阻塞
[main] 判斷merge返回的通道t是否關閉
step10:
[sub(sq)] out通道開發,判斷通道in是否關閉
[main] 通道t沒有關閉,並輸出t,此時t通道不在阻塞
step11:
[sub(sq)]in通道關閉(step8),close(out),協程結束
[main] t中沒有值,不進行操作
[sub(merge)]out通道不再阻塞,判斷c通道是否關閉
step12:
[sub(merge)] c通道關閉(step11) close(out) 協程結束
step13:
[sub(wait)]因爲step12中協程結束,所以close(out)
step14:
[main]判斷通道t關閉(step13),結束程序
流程圖總結
- 只有等待,阻塞後輸入,通道空後讀出纔會造成協程跳過一些step。
- main主線程因爲要等到通道t關閉,也就是要等待sub(wait)結束,而sub(wait)要等到所有的協程結束,所以肯定不會發生主線程跑完但是協程還沒有跑完的情況。
流水線示意圖
這是對 go中關於chan應用的程序分析
文章中代碼的流水流水線示意圖。
- 我們假設gen,sq和print操作需要的時間單位都是1:
time | gen | sq | |
---|---|---|---|
1 | out:1 | ||
2 | out:2 | out: 1 | |
3 | out:3 | out: 4 | 1 |
4 | out:4 | out: 9 | 4 |
5 | out:5 | Out: 16 | 9 |
6 | out:6 | Out: 25 | 16 |
7 | out:7 | Out: 36 | 25 |
8 | out:8 | Out: 49 | 36 |
9 | close(out) | close(out) | done<- |
我們假設gen,sq和print沒一個處理步驟是相同的,那我們就可以明顯的看到一個流水線的形式,而最後我們給done這個通道推入數據,gen,sq同時關閉並退出。
- 但是在程序的主體運行部分,sq函數通常需要更多的時間進行數據運算或其他相關的操作。我們假設sq的需要2個時間單位。
time | gen | sq | |
---|---|---|---|
1 | 1 | ||
2 | 2 | 1 | |
3 | blocked | going | |
4 | 3 | 4 | 1 |
5 | blocked | going | waiting |
6 | 4 | 9 | 4 |
7 | blocked | going | waiting |
8 | 5 | 16 | 9 |
9 | close(out) | close(out) | done<- |
由於sq需要2個時間單位來進行運算,這就導致了上游方法gen輸出通道阻塞,和下游方法print讀取通道等待。增加了時間的消耗。
- 由於sq需要消耗2個時間單位所以我們開啓2個sq函數,同時用一個merge函數處理兩個sq函數的通道輸出。如下:
time | gen | Sq1 | Sq2 | Merge | |
---|---|---|---|---|---|
1 | 1 | ||||
2 | 2 | 1 | |||
3 | 3 | Going | 4 | ||
4 | 4 | 9 | Going | 1 | |
5 | 5 | Going | 16 | 4 | 1 |
6 | 6 | 25 | Going | 9 | 4 |
7 | 7 | Going | 36 | 16 | 9 |
8 | 8 | 49 | Going | 25 | 16 |
9 | close(out) | close(out) | close(out) | close(out) | done<- |
在這個流水線中,用了兩個sq和一個merge作爲中間函數來進行數據處理。這樣同樣在9個時間單位內我們可以輸出4個值,同時也不會造成阻塞等行爲。
- 既然這樣我們開3個通道會如何呢?
time | gen | Sq1 | Sq2 | Sq3 | Merge | |
---|---|---|---|---|---|---|
1 | 1 | |||||
2 | 2 | 1 | ||||
3 | 3 | Going | 4 | |||
4 | 4 | Going | 9 | 1 | ||
5 | 5 | 16 | Going | 4 | 1 | |
6 | 6 | 25 | Going | 9 | 4 | |
7 | 7 | Going | 36 | 16 | 9 | |
8 | 8 | Going | 49 | 25 | 16 | |
9 | close(out) | close(out) | close(out) | close(out) | done<- |
這時可以看出,如果通道開的多了之後總會有一部分通道沒有處於滿負荷運轉,這樣會增加我們的空間消耗。
- 而如果sq需要華爲3個時間單位呢?
time | gen | Sq1 | Sq2 | Sq3 | Merge | |
---|---|---|---|---|---|---|
1 | 1 | |||||
2 | 2 | 1 | ||||
3 | 3 | Going | 4 | |||
4 | 4 | Going | Going | 9 | ||
5 | 5 | 16 | Going | Going | 1 | |
6 | 6 | Going | 25 | Going | 4 | 1 |
7 | 7 | Going | Going | 36 | 9 | 4 |
8 | 8 | 49 | Going | going | 16 | 9 |
9 | close(out) | close(out) | close(out) | close(out) | done<- |
在這個流水線中,開三個通道,同時sq的處理時間是3個時間單位,正好每個協程都在運轉且不會造成阻塞或等待的現象。
由此可知,我們在一個pipeline中,其中的一個部分是否並行主要根據各子程序運行一次的時間來判斷,運行時間越長,越應該用多個協程處理。
用拓撲圖進行表示:
整體總結
使用go就不可避免的要用到協程和通道,耐心的分析是寫好代碼的基礎。當不了解程序的運行過程時,我們需要儘可能清晰的整理程序運行的流程。
在理解了程序的運行原理之後,就可以根據實際情況分析在什麼時候使用多箇中間者,使用多少箇中間者,怎麼進行收束等。(下面會給出使用多中間者的一個稍微複雜一點的代碼,這時我在工作中使用的一個go的腳本)
之前的問題是否已經迎刃而解
-
爲什麼整個程序在主程序中沒有等待協程程序結束的類似wg.Wait的語句,仍然程序可以正確的輸出?
-
爲什麼在merge中wg.Wait是需要另外起一個協程,而不是在函數的主程序中進行wait?
-
make(chan int)和make(chan int ,1)的區別,在本程序中能把所有的chan聲明從前者換成後者嗎?
-
本程序是把php的一個數組通過一定的映射關係,轉換爲json格式的字符串,並保存到文件中。
更多閱讀
- 併發與並行不同 原視頻
- pipeline(這是文章代碼出處)後續會有翻譯版本
- [翻譯]Go併發模式:構建和終止流水線
- go pipeline的流水線示意圖
package main
import (
"encoding/json"
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"io/ioutil"
"regexp"
"strings"
"sync"
)
var php = `
[
'耽美同人' => ['first_id' => 2, 'name' => '純愛小說', 'third' => '百合純愛'],
'懸疑推理' => ['first_id' => 2, 'name' => '懸疑靈異', 'third' => '恐怖驚悚'],
]
`
type Item struct {
FirstID int `json:"first_id"`
Name string `json:"name"`
Third string `json:"third"`
}
type CategoryReplaceMap struct {
ID int `json:"id"`
Before string `json:"before"`
After string `json:"after"`
}
type Result struct {
CopyrightCateGory string `json:"copyright_category"`
First *CateGoryField `json:"stack_first_category_id"`
Second *CateGoryField `json:"stack_second_category_id"`
Third *CateGoryField `json:"stack_third_category_id"`
}
type Response struct {
CopyrightCateGory string `json:"copyright_category"`
First int `json:"stack_first_category_id"`
Second int `json:"stack_second_category_id"`
Third int `json:"stack_third_category_id"`
}
type CateGoryField struct {
Name string `json:"name"`
Parent int `json:"parent"`
Rank int `json:"rank"`
ID int `json:"id"`
}
func main() {
phpMap, err := parseToMap()
if err != nil {
fmt.Println("解析成map錯誤")
return
}
response := make(map[string][]Response)
s := product(phpMap)
v := make([]<-chan *Response, len(phpMap))
for i := 0; i < len(phpMap); i++ {
v[i] = middle(s)
}
out := merge(v)
for item := range out {
response["category_mappers"] = append(response["category_mappers"], *item)
}
findNotMap(phpMap, response)
resultJson, err := json.Marshal(response)
ioutil.WriteFile("test.json", resultJson, 0777)
fmt.Println(string(resultJson), err)
}
func parseToMap() (map[string]*Item, error) {
var phpMap = make(map[string]*Item)
rePlaceMap, err := getReplaceMapFromDB()
if err != nil {
fmt.Println(err)
}
fmt.Println("分類改變的對應關係")
for k, v := range rePlaceMap {
fmt.Println(k, v)
}
phpjson := strings.ReplaceAll(php, " ", "")
for k, v := range rePlaceMap {
phpjson = strings.ReplaceAll(phpjson, k, v)
}
phpjson = strings.Replace(phpjson, "=>", ":", -1)
phpjson = strings.Replace(phpjson, "'", "\"", -1)
phpjson = strings.Replace(phpjson, "[", "{", -1)
phpjson = strings.Replace(phpjson, "]", "}", -1)
reg1 := regexp.MustCompile(`([\d]+) *:`)
if reg1 == nil {
fmt.Println("regexp err")
return nil, errors.New("匹配模式錯誤1")
}
//根據規則提取關鍵信息
phpjson = reg1.ReplaceAllString(phpjson, `"${1}" :`)
reg2 := regexp.MustCompile(`(}),(\n *})`)
if reg2 == nil {
fmt.Println("regexp err")
return nil, errors.New("匹配模式錯誤1")
}
//根據規則提取關鍵信息
phpjson = reg2.ReplaceAllString(phpjson, `${1}${2}`)
fmt.Println(phpjson)
err = json.Unmarshal([]byte(phpjson), &phpMap)
fmt.Println(err)
return phpMap, err
}
//product是把正常的輸入傳入通道,
func product(in map[string]*Item) <-chan map[string]*Item {
out := make(chan map[string]*Item)
go func() {
for k, v := range in {
out <- map[string]*Item{k: v}
}
close(out)
}()
return out
}
//middle 是一個處理數據的函數,
func middle(in <-chan map[string]*Item) <-chan *Response {
out := make(chan *Response)
go middleHand(in, out)
return out
}
func middleHand(in <-chan map[string]*Item, out chan *Response) {
for aMap := range in {
res := &Response{}
for k, v := range aMap {
dbRes := getDBByNameRes(v.Third)
wg := sync.WaitGroup{}
for _, item := range dbRes {
wg.Add(1)
go func(item *CateGoryField) {
defer func() {
wg.Done()
if err := recover(); err != nil {
fmt.Printf("book, panic: %+v\n", err)
}
}()
resultItem := &Result{}
resultItem.CopyrightCateGory = k
getResult(item, resultItem)
if resultItem.First.ID == v.FirstID && resultItem.Second.Name == v.Name {
responseItem := &Response{}
responseItem.CopyrightCateGory = resultItem.CopyrightCateGory
responseItem.First = resultItem.First.ID
responseItem.Second = resultItem.Second.ID
responseItem.Third = resultItem.Third.ID
res = responseItem
}
}(item)
}
wg.Wait()
}
out <- res
}
close(out)
}
//merge是把middle的輸出整合後傳入一個通道
func merge(in []<-chan *Response) <-chan *Response {
out := make(chan *Response)
wg := sync.WaitGroup{}
output := func(c <-chan *Response) {
for n := range c {
out <- n
}
wg.Done()
}
for _, i := range in {
wg.Add(1)
go output(i)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
func getResult(item *CateGoryField, resultItem *Result) {
if item.Rank == 1 {
resultItem.First = item
}
if item.Rank == 2 {
resultItem.Second = item
dbRes := getDBByID(item.Parent)
getResult(dbRes, resultItem)
}
if item.Rank == 3 {
resultItem.Third = item
dbRes := getDBByID(item.Parent)
getResult(dbRes, resultItem)
}
}
func getReplaceMapFromDB() (map[string]string, error) {
res := []*CategoryReplaceMap{}
db, err:= connectDB("distribution_w:********@tcp(rm-******.mysql.******.rds.aliyuncs.com:3306)/content_distribution?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil{
return nil,err
}
err = db.Table("category_change").Find(&res).Error
if err != nil {
fmt.Println("查詢數據庫失敗")
return nil, err
}
response := make(map[string]string)
for _, item := range res {
item.Before = strings.ReplaceAll(item.Before, "\n", "")
item.After = strings.ReplaceAll(item.After, "\n", "")
item.Before = strings.ReplaceAll(item.Before, " ", "")
item.After = strings.ReplaceAll(item.After, " ", "")
response[item.Before] = item.After
}
return response, nil
}
func getDBByNameRes(name string) []*CateGoryField {
res := make([]*CateGoryField, 0)
//res:= execsql(sql)
db, err:= connectDB("distribution_w:********@tcp(rm-******.mysql.******.rds.aliyuncs.com:3306)/content_distribution?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil{
return nil
}
err = db.Table("category").Where("name = ? ", name).Find(&res).Error
if err != nil {
fmt.Println("查詢數據庫失敗")
return nil
}
return res
}
func getDBByID(id int) *CateGoryField {
res := &CateGoryField{}
db, err:= connectDB("distribution_w:********@tcp(rm-******.mysql.******.rds.aliyuncs.com:3306)/content_distribution?charset=utf8mb4&parseTime=True&loc=Local")
if err != nil{
return nil
}
err = db.Table("category").Where("id = ? ", id).Find(&res).Error
if err != nil {
fmt.Println("查詢數據庫失敗")
return nil
}
return res
}
func connectDB(connect string) (*gorm.DB, error) {
db, err := gorm.Open("mysql", connect)
if err != nil {
fmt.Println("連接數據庫失敗")
return nil,err
}
return db,nil
}
func findNotMap(phpMap map[string]*Item, response map[string][]Response) {
for k := range phpMap {
var yn bool
for _, v := range response["category_mappers"] {
if k == v.CopyrightCateGory {
yn = true
break
}
}
if yn {
continue
}
fmt.Println(k, phpMap[k], " not in the category_mappers")
}
}