golang的兩把利器,協(xié)程和管道

golang的協(xié)程相信大家都不陌生,在golang中的使用也很簡單熄诡,只要加上一個關(guān)鍵字go即可,雖然說大家都知道冬殃,但是真的在實際使用中又遇到這樣那樣的問題霎烙,坑其實還是挺多的。而網(wǎng)上很多文章和教程赡茸,要么就是講的太簡單缎脾,給你簡單介紹一下協(xié)程和管道的使用,點到為止占卧,要么就上來給你擼GPM模型遗菠,看的人一臉懵逼联喘,所以我以實際使用過程中遇到的問題這個角度出發(fā),可能會分多篇總結(jié)一下golang的協(xié)程相關(guān)的知識點,希望對你有用辙纬,如果覺得還不錯豁遭,記得點個贊,點個關(guān)注牲平。

ps:如果你從來沒有了解過golang的協(xié)程堤框,建議先自己搜一些資料簡單的了解一下,還有并發(fā)并行那些基礎(chǔ)概念之類的纵柿,本文都不會提及蜈抓。

協(xié)程非常容易引發(fā)并發(fā)問題

我們先看下列程序

func main() {
    res := make(map[int]int)
    for i := 0; i < 100; i++ {
        go handleMap(res)
    }
    time.Sleep(time.Second * 1)

}
func handleMap(res map[int]int) {
    for i := 0; i < 200; i++ {
        res[i] = i * i
    }
}
  • 因為map類型作為參數(shù)是直接以引用的方式傳遞的,所以handleMap函數(shù)不需要返回值昂儒,直接操作參數(shù)res即可
  • handleMap的作用就是不斷的給map賦值
  • 因為執(zhí)行handleMap的時候是開啟協(xié)程的沟使,所以是多個程序并發(fā)的去對res(map類型寫入),所以上述程序是會報錯的渊跋,輸出結(jié)果如下
  • 程序下方加上time.Sleep(time.Second * 1)的原因是因為主程序(main)執(zhí)行完畢退出腊嗡,但是協(xié)程還沒執(zhí)行完畢會被直接關(guān)閉。
fatal error: concurrent map writes

goroutine 48 [running]:
runtime.throw(0x100f814d1, 0x15)
        /opt/homebrew/Cellar/go@1.16/1.16.13/libexec/src/runtime/panic.go:1117 +0x54 fp=0x14000145f50 sp=0x14000145f20 pc=0x100f16f34
runtime.mapassign_fast64(0x100faeae0, 0x14000106180, 0x1f, 0x14000072200)
        /opt/homebrew/Cellar/go@1.16/1.16.13/libexec/src/runtime/map_fast64.go:176 +0x2f8 fp=0x14000145f90 sp=0x14000145f50 pc=0x100ef7188
main.handleMap(0x14000106180)
        /Users/test/Sites/beikego/test/rountine.go:22 +0x44 fp=0x14000145fd0 sp=0x14000145f90 pc=0x100f7e644
runtime.goexit()

解決方式(1) 加鎖

如果有并發(fā)問題拾酝,我們最容易想到的一個辦法就是加鎖

func main() {
    res := make(map[int]int)
    for i := 0; i < 100000; i++ {
        go handleMap(res)
    }

    time.Sleep(time.Second * 1)
    lock.Lock()  //因為對map的讀取的時候有可能還在寫入燕少,所以這里也需要加鎖
    for _, item := range res {
        fmt.Println(item)
    }
    lock.Unlock()
}
func handleMap(res map[int]int) {
    lock.Lock()  //每一個協(xié)程過來請求都先加鎖
    for i := 0; i < 2000; i++ {
        res[i] = i * i
    }
    lock.Unlock()  //處理完map之后釋放鎖
}

上面過程我畫了一張圖,具體哪里為什么加鎖都有說明

上述程序執(zhí)行過程圖示
  • 上述例子雖然開啟了100000個協(xié)程蒿囤,但是在每個協(xié)程處理map的時候加上了一個lock客们,處理完畢才釋放,所以各個協(xié)程對map的操作是隔離開的材诽。
  • 在讀取map的時候加鎖的原因底挫,是因為sleep 1s之后,有可能map還在寫入脸侥,邊讀邊寫當(dāng)然會有并發(fā)問題
    上述方式雖然解決了并發(fā)問題建邓,但是也存在一定的問題。主要是需要sleep睁枕,而且sleep多長時間沒法確定
    所以這里引入咱們的解決方式2官边,管道

解決方式(2)管道channel

channel本質(zhì)就是一個數(shù)據(jù)結(jié)構(gòu),隊列譬重。既然是隊列拒逮,當(dāng)然有著先進先出的原則,而且是能保證線程安全的,多個gorountine訪問不需要加鎖。

當(dāng)然如果你還沒有接觸過管道臀规,可以提前找些資料了解一下滩援,下面是一個管道的簡單示意圖


管道簡單示意圖

管道在使用的過程中需要注意的問題

管道(channel)在使用的過程中有很多需要注意的點,我在這里列一下

使用管道之前必須make一下塔嬉,而且指定長度

  var intChan chan int
    intChan <- 1
    fmt.Println(<-intChan)
  //返回信息
  fatal error: all goroutines are asleep - deadlock!
  goroutine 1 [chan send (nil chan)]:

為什么需要make玩徊,前面文章已經(jīng)講過租悄,可以看看,
聊聊golang的make和new函數(shù)
指定長度也很好理解恩袱,管道的本質(zhì)是隊列,隊列當(dāng)然是需要指定長度的

管道寫入的數(shù)據(jù)數(shù)如果超過管道長度泣棋,會報錯

  intChan := make(chan int, 1)  //長度為1
    intChan <- 1
    intChan <- 2  //這里會報錯
    fmt.Println(<-intChan)
  //返回結(jié)果
  fatal error: all goroutines are asleep - deadlock!
  goroutine 1 [chan send]:

讀取空管道,會報錯

intChan := make(chan int, 1)
fmt.Println(<-intChan)  //此時管道里面還沒有任何內(nèi)容
//返回結(jié)果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:

管道也支持interface畔塔,但是拿到結(jié)構(gòu)體具體的屬性的時候潭辈,需要斷言

type Person struct {
    Name string
}
func main(){
    personChan := make(chan interface{}, 10)
      personChan <- Person{Name: "小飯"}  //寫入結(jié)構(gòu)體類型
      personChan <- 1  //寫入int類型
      personChan <- "test_string"  //寫入string類型
      fmt.Println(<-personChan, <-personChan, <-personChan)
}
  //返回結(jié)果
  {小飯} 1 test_string

上面例子我們可以看到,如果管道定義為interface類型澈吨,任何類型的數(shù)據(jù)都是可以寫入并且正常取出的把敢,但是我們寫入結(jié)構(gòu)體類型之后,如果想取出結(jié)構(gòu)體的具體屬性谅辣,則需要斷言

type Person struct {
    Name string
}
func main() {
    personChan := make(chan interface{}, 10)
    personChan <- Person{Name: "小飯"}
    person := <-personChan  //取出結(jié)構(gòu)體之后修赞,此時還不知道是什么類型,所以沒法直接取屬性桑阶,因為定義的是interface
    per := person.(Person)  //對取出結(jié)果進行斷言
    fmt.Println(per.Name)
}
//返回結(jié)果
小飯

管道是可以循環(huán)的柏副,但是循環(huán)之前必須關(guān)閉,關(guān)閉之后不可寫入任何數(shù)據(jù)

  personChan := make(chan int, 10)
    personChan <- 1
    personChan <- 2
    personChan <- 3
    close(personChan) //關(guān)閉之后管道不能寫入任何數(shù)據(jù)蚣录,否則就會報 panic: send on closed channel
    for item := range personChan {  //在for range循環(huán)管道之前必須關(guān)閉管道割择,否則會報  fatal error: all goroutines are asleep - deadlock!
        fmt.Println(item)
    }
  • 其實為什么循環(huán)之前需要關(guān)閉管道,很好理解萎河,因為for rang循環(huán)可以簡單理解為一個死循環(huán)锨推,當(dāng)管道數(shù)據(jù)讀取完了之后會繼續(xù)讀取,類似于讀取一個空管道公壤,當(dāng)然會報錯
  • 管道關(guān)閉之后不能寫入更好理解,一個對象銷毀了還能去賦值么椎椰?一樣的道理

切忌不要嘗試用for(i:=0;i<len(chan):i++)的方式去循環(huán)

這個很好理解厦幅,我就不用代碼演示了,因為每次從管道中取一個數(shù)據(jù)慨飘,len(chan)是變化的确憨,所以這么取數(shù)據(jù)肯定是有問題的。換句話說也就是不要隨便用len(chan),坑很多

協(xié)程和管道的綜合使用

我們前面拋出的問題是瓤的,開啟協(xié)程操作map會引發(fā)并發(fā)問題休弃,現(xiàn)在我們看看怎么用管道解決他

協(xié)程和管道配合解決map寫入并發(fā)問題
  • 注意這里用到了兩個管道,管道chan map是用于map的讀寫用的圈膏,exitChan是用于告訴main函數(shù)可以退出用的
  • 首先開啟一個writeMap的協(xié)程塔猾,把map數(shù)據(jù)都寫入到管道(chan map)中,需要注意的是數(shù)據(jù)寫完之后需要把協(xié)程關(guān)閉掉
  • 在開啟一個readMap的協(xié)程稽坤,把管道中(chan map)數(shù)據(jù)一個一個的讀出來.
  • 當(dāng)readMap把數(shù)據(jù)全部讀取完成中后丈甸,給main函數(shù)發(fā)送一個信號(也就是往exitChan中寫一條數(shù)據(jù))
  • main函數(shù)監(jiān)聽exitChan糯俗,收到數(shù)據(jù)直接退出即可。
var chanMap chan map[int]int
var exitChan chan int

func main() {
    size := 50000
    chanMap := make(chan map[int]int, size)  
    exitChan := make(chan int, 1)
    go WriteMap(chanMap, size)  //開啟寫map協(xié)程
    go ReadMap(chanMap, exitChan) //開啟讀map協(xié)程
    for {
        exit := <-exitChan  //監(jiān)聽exitChan 收到信號直接return即可
        if exit != 0 {
            return
        }
    }
}

//寫map數(shù)據(jù)
func WriteMap(chanMap chan map[int]int, size int) {
    for i := 1; i <= size; i++ {
        temp := make(map[int]int, 1)
        temp[i] = i
        chanMap <- temp
        fmt.Println("寫入數(shù)據(jù):", temp)
    }
    close(chanMap)  //注意數(shù)據(jù)寫完需要關(guān)閉管道
}
//讀map數(shù)據(jù)
func ReadMap(chanMap chan map[int]int, exitChan chan int) {
    for {
        val, ok := <-chanMap
        if !ok {
            break
        }
        fmt.Println("讀取到:", val)
    }
    exitChan <- 1  //數(shù)據(jù)讀取完畢通知main函數(shù)可退出
}

協(xié)程和管道到底能提升多高的效率睦擂?

咱們用協(xié)程的目的就是想提高程序的運行效率得湘,管道可以簡單理解為是協(xié)助協(xié)程一起使用的,但是效率到底能提升多少呢顿仇?咱們一起來看一看淘正。

判斷素數(shù)

大家都知道,判斷素數(shù)的復(fù)雜度是N2臼闻,比較慢鸿吆,咱們先看一看傳統(tǒng)的一個一個的去判斷需要多長時間

判斷100000以內(nèi)的數(shù)字哪些是素數(shù)
func CheckPrime(num int) bool {  //判斷一個數(shù)字是否是素數(shù)
    res := true
    for i := 2; i < num; i++ {
        if num%i == 0 {
            res = false
        }
    }
    return res
}


func main(){
  t := time.Now()
    size := 100000

    for i := 0; i < size; i++ {
        if CheckPrime(i) {
            fmt.Println(i, "是素數(shù)")
        }
    }
    elapsed := time.Since(t)

    fmt.Println("app elapsed:", elapsed)
    return
}

上述程序運行了3.33秒多,看來還是比較慢的

接下來我們用協(xié)程和管道的方式看看些阅,還是老規(guī)矩伞剑,我們先看看流程圖

協(xié)程和管道配合查找素數(shù)
  • 先把每個需要判斷的數(shù)字寫入initChan
  • 開啟多個協(xié)程拉取initChan的數(shù)據(jù)一個一個的判斷,這一步是程序速度加快的關(guān)鍵,如果不是素數(shù)市埋,不處理即可黎泣,如果是素數(shù),就寫入PrimeChan缤谎,判斷完之后寫入exitChan抒倚,通知主程序即可
  • 主程序監(jiān)聽primeChan并輸出,同時監(jiān)聽exitChan坷澡,收到信號退出即可
//初始化托呕,把需要被判斷的數(shù)字寫入initChan
func initChan(intChan chan int, size int) {
    for i := 1; i <= size; i++ {
        intChan <- i
    }
    close(intChan)
}
//讀取initChan中的數(shù)據(jù),一個一個的判斷频敛,如果是素數(shù)项郊,就寫入PrimeChan,并且寫入exitChan

func CheckPrimeChan(intChan, primeChan chan int, exitChan chan bool) {
    for {
        num, ok := <-intChan
        if !ok {
            break
        }
        if CheckPrime(num) {
            primeChan <- num
        }
    }
    exitChan <- true

}

func main() {
    t := time.Now() 
    size := 100000
    intChan := make(chan int, size)
    primeChan := make(chan int, size)
    exitChan := make(chan bool, 1)
    go initChan(intChan, size) //初始化initChan
    checkChannelNum := 8
    for i := 0; i < checkChannelNum; i++ {  //開啟8個協(xié)程同時拉取initChan的數(shù)據(jù)并判斷是否是素數(shù)
        go CheckPrimeChan(intChan, primeChan, exitChan)
    }
    go func() {
        for i := 0; i < checkChannelNum; i++ {
            <-exitChan
        }
        close(primeChan)

    }()

    for {
        value, ok := <-primeChan
        if !ok {
            break
        }
        fmt.Println(value, "是素數(shù)")
    }
    elapsed := time.Since(t)

    fmt.Println("app elapsed:", elapsed)
}
  //程序執(zhí)行消耗時間
  848.455084m

上述程序執(zhí)行時間為848.455084ms,是傳統(tǒng)的方式的時間的四分之一斟赚,可見協(xié)程在提高運行效率這塊的作用還是顯而易見的

本文由mdnice多平臺發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末着降,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拗军,更是在濱河造成了極大的恐慌任洞,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件发侵,死亡現(xiàn)場離奇詭異交掏,居然都是意外死亡,警方通過查閱死者的電腦和手機刃鳄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進店門盅弛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事熊尉」蘖” “怎么了?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵狰住,是天一觀的道長张吉。 經(jīng)常有香客問我,道長催植,這世上最難降的妖魔是什么肮蛹? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮创南,結(jié)果婚禮上伦忠,老公的妹妹穿的比我還像新娘。我一直安慰自己稿辙,他們只是感情好昆码,可當(dāng)我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邻储,像睡著了一般赋咽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吨娜,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天脓匿,我揣著相機與錄音,去河邊找鬼宦赠。 笑死陪毡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勾扭。 我是一名探鬼主播毡琉,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妙色!你這毒婦竟也來了绊起?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤燎斩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蜂绎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栅表,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年师枣,在試婚紗的時候發(fā)現(xiàn)自己被綠了怪瓶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡践美,死狀恐怖洗贰,靈堂內(nèi)的尸體忽然破棺而出找岖,到底是詐尸還是另有隱情,我是刑警寧澤敛滋,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布许布,位于F島的核電站,受9級特大地震影響绎晃,放射性物質(zhì)發(fā)生泄漏蜜唾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一庶艾、第九天 我趴在偏房一處隱蔽的房頂上張望袁余。 院中可真熱鬧,春花似錦咱揍、人聲如沸颖榜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掩完。三九已至,卻和暖如春积暖,著一層夾襖步出監(jiān)牢的瞬間藤为,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工夺刑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缅疟,地道東北人。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓遍愿,卻偏偏與公主長得像存淫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沼填,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容