<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          不要忽略 goroutine 的啟動(dòng)時(shí)間

          共 4157字,需瀏覽 9分鐘

           ·

          2020-08-26 13:09


          小菜刀的項(xiàng)目中需要將數(shù)據(jù)推給多個(gè)服務(wù)器,大致如下


           1package?main
          2
          3import?(
          4????"fmt"
          5????"sync"
          6)
          7
          8func?mockSendToServer(url?string)?{
          9????fmt.Printf("server?url:?%s\n",?url)
          10}
          11
          12func?main()?{
          13????urls?:=?[]string{"0.0.0.0:5000",?"0.0.0.0:6000",?"0.0.0.0:7000"}
          14????wg?:=?sync.WaitGroup{}
          15????for?_,?url?:=?range?urls?{
          16????????wg.Add(1)
          17????????go?func()?{
          18????????????defer?wg.Done()
          19????????????mockSendToServer(url)
          20????????}()
          21????}
          22????wg.Wait()
          23}


          請(qǐng)讀者停來(lái)下思考一下,以上代碼會(huì)得到什么樣的輸出。


          1$?go?run?main.go?
          2server?url:?0.0.0.0:7000
          3server?url:?0.0.0.0:7000
          4server?url:?0.0.0.0:7000


          這個(gè)結(jié)果,是不是和你想的一樣呢。那么,問(wèn)題出在了哪里?為什么從for循環(huán)中傳遞的url變得相同了,且為數(shù)組中的最后一項(xiàng)url。


          原因分析


          在Go中提供了非常好的程序分析工具,trace和pprof。trace側(cè)重于分析goroutine的調(diào)度,pprof則側(cè)重于程序的運(yùn)行性能。為了更好地排查上述bug產(chǎn)生的原因,代碼中新增了trace代碼段。


           1package?main
          2
          3import?(
          4????"fmt"
          5????"os"
          6????"runtime/trace"
          7????"sync"
          8)
          9
          10func?mockSendToServer(url?string)?{
          11????fmt.Printf("server?url:?%s\n",?url)
          12}
          13
          14func?main()?{
          15????f,?err?:=?os.Create("trace.out")
          16????if?err?!=?nil?{
          17????????panic(err)
          18????}
          19????defer?f.Close()
          20
          21????err?=?trace.Start(f)
          22????if?err?!=?nil?{
          23????????panic(err)
          24????}
          25????defer?trace.Stop()
          26????urls?:=?[]string{"0.0.0.0:5000",?"0.0.0.0:6000",?"0.0.0.0:7000"}
          27????wg?:=?sync.WaitGroup{}
          28????for?_,?url?:=?range?urls?{
          29????????wg.Add(1)
          30????????go?func()?{
          31????????????defer?wg.Done()
          32????????????mockSendToServer(url)
          33????????}()
          34????}
          35????wg.Wait()
          36}


          運(yùn)行命令go?run?main.go? ,將在同級(jí)目錄生成trace.out文件。此時(shí),執(zhí)行go tool命令。


          1go?tool?trace?trace.out
          22020/08/15?16:35:58?Parsing?trace...
          32020/08/15?16:35:58?Splitting?trace...
          42020/08/15?16:35:58?Opening?browser.?Trace?viewer?is?listening?on?http://127.0.0.1:61272


          在web瀏覽器(推薦使用Chrome)打開http://127.0.0.1:61272


          在這里,我們只關(guān)心兩項(xiàng)指標(biāo)。第一行View trace(可視化整個(gè)程序的調(diào)度流程)和第二行Groutine analysis。相信讀者對(duì)trace和pprof的基本玩法都有了解,這里就不做過(guò)多介紹。后續(xù)小菜刀也會(huì)盡力出一些關(guān)于它們使用的詳細(xì)文章。


          進(jìn)入Goroutine analysis項(xiàng)。



          可以看到,程序一共有5個(gè)goroutine,分別是三個(gè)for循環(huán)里啟動(dòng)的匿名go func()、一個(gè)trace.Start.func1runtime.main


          進(jìn)入main.main.func1


          這三個(gè)代表的就是for循環(huán)結(jié)構(gòu)體里面啟動(dòng)的三個(gè)goroutine。這里,請(qǐng)記住它們的編號(hào)19、20、21。


          此時(shí),退回到http://127.0.0.1:61272頁(yè)面,進(jìn)入View trace項(xiàng)。





          點(diǎn)擊G1(G1代表的是runtime.main)所在的綠色方框,得到如下信息



          上圖是重點(diǎn)!我們可以看到G18、G19、G20、G21都是通過(guò)G1衍生出來(lái)的。同時(shí)可以看到,G1運(yùn)行阻塞于第35行代碼處,即wg.Wait()


          但是,通過(guò)上圖中g(shù)oroutine的運(yùn)行時(shí)序,此時(shí)for循環(huán)中的3個(gè)goroutine均還未成功啟動(dòng)運(yùn)行(雖然已經(jīng)在主goroutine即main中通過(guò)go關(guān)鍵字進(jìn)行了goroutine調(diào)用聲明)。


          那么,到這里讀者應(yīng)該清楚上文中bug產(chǎn)生的原因了吧。


          1for?_,?url?:=?range?urls?{
          2????wg.Add(1)
          3????go?func()?{
          4????????defer?wg.Done()
          5????????mockSendToServer(url)
          6????}()
          7}
          8wg.Wait()


          原因

          當(dāng)主goroutine中的for循環(huán)邏輯已經(jīng)走完并阻塞于wg.Wait()一段時(shí)間后,go func的goroutine才啟動(dòng)準(zhǔn)備(準(zhǔn)備資源,掛載M線程等)完畢。那么,此時(shí)三個(gè)goroutine中獲取的url都是指向的最后一次for循環(huán)的url,因此就造成了上文中的bug。

          goroutine的啟動(dòng)時(shí)間是多少


          通過(guò)在小菜刀電腦上的多次運(yùn)行觀察,一次goroutine的啟動(dòng)準(zhǔn)備時(shí)間在數(shù)十微秒左右。當(dāng)然該值在不同的操作系統(tǒng)和硬件設(shè)備上肯定會(huì)存在一些差異。為此,小菜刀在程序以下部分做了些小改動(dòng),以做測(cè)試


           1for?i,?url?:=?range?urls?{
          2????wg.Add(1)
          3????go?func()?{
          4????????defer?wg.Done()
          5????????mockSendToServer(url)
          6????}()
          7????if?i?==?1?{
          8????????//在讀取url為"0.0.0.0:6000"時(shí),睡50微秒
          9????????time.Sleep(time.Microsecond?*?50)
          10????}
          11}
          12wg.Wait()


          程序多次運(yùn)行會(huì)產(chǎn)生不同的結(jié)果


           1?$?go?run?main.go?
          2server?url:?0.0.0.0:6000
          3server?url:?0.0.0.0:7000
          4server?url:?0.0.0.0:6000
          5
          6?$?go?run?main.go?
          7server?url:?0.0.0.0:6000
          8server?url:?0.0.0.0:6000
          9server?url:?0.0.0.0:7000
          10
          11?$?go?run?main.go?
          12server?url:?0.0.0.0:6000
          13server?url:?0.0.0.0:7000
          14server?url:?0.0.0.0:7000


          那么,我們就選取6000、7000、7000的結(jié)果來(lái)分析下當(dāng)時(shí)goroutine的啟動(dòng)和調(diào)度情況。



          如上,可以得知:由于在第二次for循環(huán)中讓主goroutine睡了50微秒,使得首次被主goroutine調(diào)起的go func(上圖表現(xiàn)為G20)已經(jīng)得到了充足的時(shí)間來(lái)準(zhǔn)備啟動(dòng)。但是首次調(diào)起的go func,其獲取url的時(shí)間片是在第二次循環(huán)的睡眠階段,因此它得到的url是"0.0.0.0:6000",而其他兩個(gè)go func(G21和G19)最終運(yùn)行時(shí)獲取的值還是"0.0.0.0:7000"的url。


          之所以睡眠50微秒會(huì)造成不同的結(jié)果,是由于goroutine的啟動(dòng)時(shí)間并不固定,會(huì)存在一定范圍的波動(dòng)。


          因此,解決上文的bug的方案應(yīng)為如下


          1for?_,?url?:=?range?urls?{
          2????wg.Add(1)
          3????go?func(url?string)?{
          4????????defer?wg.Done()
          5????????mockSendToServer(url)
          6????}(url)
          7}
          8wg.Wait()


          將每次遍歷的url所指向值,通過(guò)函數(shù)入?yún)ⅲ鳛閿?shù)據(jù)資源賦予給go func,這樣不管goroutine啟動(dòng)會(huì)有多耗時(shí),其url已經(jīng)作為goroutine的私有數(shù)據(jù)保存,后續(xù)運(yùn)行就用上了正確的url,那么,上文bug也相應(yīng)解除。


          后記


          小菜刀在線上遇到該bug時(shí),雖然已經(jīng)知道通過(guò)url入?yún)⒌姆绞竭M(jìn)行修改,但當(dāng)時(shí)沒(méi)有過(guò)多思考,以為問(wèn)題是出在了for...range的值拷貝上面。通過(guò)后續(xù)和同事的討論與自己多次不同嘗試之后,才意識(shí)到原來(lái)是goroutine的啟動(dòng)時(shí)間在搗鬼。


          最后,希望讀者們看完此文,不會(huì)再寫出此bug。




          推薦閱讀



          學(xué)習(xí)交流 Go 語(yǔ)言,掃碼回復(fù)「進(jìn)群」即可


          站長(zhǎng) polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)


          Go語(yǔ)言中文網(wǎng)

          每天為你

          分享 Go 知識(shí)

          Go愛好者值得關(guān)注



          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩成人电影中文字幕 | 影音先锋色婷婷 | 豆花无码AV在线 | 久久视频免费观看 | 精品黄色在线观看 |