phper學(xué)習(xí)Go之defer桨昙、panic 和 recover的實踐,最后手賤開啟二百萬協(xié)程腌歉,cpu暴漲93%

  • 作為phper,最近想了解一下Go蛙酪,但是并不代表我就放棄php了,you know ,php 想獲取一個對象的地址有多難究履!這就是靜態(tài)語言和動態(tài)語言的相差之處
  • 接下來就逐個了解一下吧滤否!

defer

defer 語句將一個函數(shù)放入一個棧中脸狸,defer 會在當(dāng)前函數(shù)返回前執(zhí)行傳入的函數(shù)最仑,經(jīng)常用于關(guān)閉文件描述符藐俺,數(shù)據(jù)庫連接,redis連接等泥彤,用于清理資源欲芹,避免資源浪費。比如下面這個栗子

package main

import (
    "fmt"
    "goapp/src/math"
)

func main() {

    sum:=math.Add(2,3)
    fmt.Println(sum)
    defer func() {fmt.Println("i am defer1")}()
    res := test_defer();
    fmt.Println(res)

}

func test_defer() float64  {
    defer func() {fmt.Println("i am defer2")}()
    defer func() {fmt.Println("i am defer3")}()
    res :=math2.Mod(5,3)
    return res;
}


執(zhí)行結(jié)果是什么呢吟吝?

  1. 執(zhí)行 一個加法菱父,打印返回值 5;

2.defer1入棧

3.執(zhí)行函數(shù)test_defer剑逃,defer2入棧浙宜,defer3入棧,執(zhí)行函數(shù)邏輯蛹磺,在return 之前 呢粟瞬,會執(zhí)行 棧里面的defer,棧嘛,先進后出萤捆,和隊列相反裙品,所以依次執(zhí)行defer3,defer2,然后返回結(jié)果

4.main函數(shù)收到test_defer的返回值俗或,開始打印結(jié)果

5.main函數(shù)在結(jié)束之前呢市怎,會執(zhí)行一下本函數(shù)內(nèi)的defer,所以開始執(zhí)行defer1

那結(jié)果是不是這樣執(zhí)行的呢?我們來看一下結(jié)果辛慰,毫無相差

image.png
那么此處可能有小伙伴要問一下区匠,defer為什么要設(shè)計成棧?
  • 通俗來講一個場景昆雀,defer 是做清場工作的辱志,對吧,那么這樣一個場景狞膘,一個小偷去倉庫偷東西揩懒,干完活了,要擦除腳印對吧挽封,那他不可能從進門的位置開始擦腳印吧已球,他只能退著擦,先擦最后一步的腳印辅愿,而且智亮,很多時候最后一步是基于前面的基礎(chǔ)上的,比如点待,還是這個小偷阔蛉,他想偷柜子里面的珠寶,那他是不是得先打開門啊癞埠,那小偷做清理工作的時候状原,不可能先關(guān)閉門聋呢,在關(guān)閉柜子吧。
  • defer是用來釋放資源的颠区,比如一個操作削锰,先給文件上鎖,然后修改文件毕莱。那defer執(zhí)行的時候應(yīng)該是先關(guān)閉文件器贩,再釋放鎖。如果釋放鎖朋截,再關(guān)閉文件蛹稍,那不是亂套了嗎?從因果關(guān)系上說部服,后分配的資源可能會依賴前面的資源稳摄,但是前面的資源肯定不會依賴后面打開的資源。所以倒過來執(zhí)行關(guān)閉 不會產(chǎn)生問題饲宿。
那有的人說厦酬,我就是讓defer先進先出,不行嗎瘫想?允許是允許仗阅,但是不提倡。哈哈国夜,是不是感受到了羅翔老師的氣場减噪,請看下面的代碼,如果defer嵌套车吹,那么defer會從外往里執(zhí)行,剝洋蔥似的筹裕,一層一層剝。
package main

func main() {
    defer func() {
        println("i am defer1")
        defer func() {
            println("i am defer2")
            defer func() {
                println("i am defer3")
            }()
        }()
    }()

    panic("i am panic")
}

image.png

panic

panic往往和recover是成對出現(xiàn)的窄驹,與defer也有緊密的聯(lián)系朝卒,panic能夠改變程序的控制流,調(diào)用panic后會立刻停止執(zhí)行當(dāng)前函數(shù)的剩余代碼乐埠,并在當(dāng)前Goroutine(協(xié)程)中遞歸執(zhí)行調(diào)用方的defer

我們看下面一段代碼

package main

import "time"

func main()  {
    defer func() {
        println("i am main defer1")
    }()

    go func() {
        defer func() {
            println("i am goroutine defer2")
        }()

        defer func() {
            println("i am goroutine defer3")
        }()

        panic("i am panic")

        defer func() {
            println("i am goroutine defer4")
        }()
    }()
    time.Sleep(1 * time.Second)
}
  • 從前面的分析我們得知以下結(jié)果
  1. defer1 入棧

2.執(zhí)行g(shù)oroutine
3.defer2 入棧
4.defer3入棧
5.panic打斷程序執(zhí)行抗斤,依次執(zhí)行defer3,defer2,panic,而panic 后面的程序不會再運行,并且main里面的defer也不會執(zhí)行

image.png
  • 我為什么要加time.Sleep 如果不加呢丈咐?
image.png

從截圖里面看到瑞眼,如果沒有time.Sleep,協(xié)程好像沒有被執(zhí)行一樣棵逊,為什么會這樣呢伤疙?因為我們知道,協(xié)程不是搶占式的辆影,如果刪除time.Sleep徒像,主goroutine不會放棄對輔助goroutine的控制花吟,但是goroutine 必須放棄控制才能運行另一個goroutine,而time.Sleep就是放棄控制的一種方法厨姚。簡單來說,你這個程序 從頭到尾都是被main 主協(xié)程占用著键菱,子協(xié)程不會主動搶占cpu,那么必須得是主協(xié)程主動讓出cpu谬墙,讓子協(xié)程有機會被cpu輪詢到,子協(xié)程才會被執(zhí)行

順道說一下什么是協(xié)程

  • 協(xié)程是go語言最重要的特色之一经备,那么我們怎么理解協(xié)程呢拭抬?協(xié)程,簡單說就是輕量級的線程侵蒙,一個協(xié)程的大小是2k 左右造虎,這也解釋了為什么go能單機百萬。
  • go語言里的協(xié)程創(chuàng)建很簡單纷闺,在go關(guān)鍵詞后面加一個函數(shù)調(diào)用就可以了算凿。代碼舉栗
package main

import "time"

func main()  {
    println("i am main goroutine")
    go func() {
        println("i am goroutine_in_1")
        go func() {
            println("i am goroutine_in_2")
            go func() {
                println("i am goroutine_in_3")
            }()
        }()
    }()

    time.Sleep(1*time.Second);
    println("main goroutine is over")
}

image.png

main 函數(shù)是怎么運行的呢?其實main函數(shù)也是運行在goroutine里面犁功,不過是主協(xié)程氓轰,上面的栗子我們是嵌套了幾個協(xié)程,但是他們中間并沒有什么層級關(guān)系浸卦,協(xié)程只有兩種署鸡,子協(xié)程和主協(xié)程。上面的代碼中限嫌,我們讓主協(xié)程休息了一秒靴庆,等待子協(xié)程返回結(jié)果。如果不讓主協(xié)程休息一秒怒医,即讓出cpu,讓子協(xié)程是沒有機會執(zhí)行的炉抒,因為主協(xié)程運行結(jié)束后,不管子協(xié)程是任何狀態(tài)稚叹,都會全部消亡端礼。

但是在實際使用中,我們要保護好每一個子協(xié)程入录,確保他們安全運行蛤奥,一個子協(xié)程的異常會傳播到主協(xié)程,直接導(dǎo)致主協(xié)程掛掉僚稿,程序崩潰凡桥。比如下面這個栗子

package main

import "time"

func main()  {
    println("i am main goroutine")
    go func() {
        println("i am goroutine_in_1")
        go func() {
            println("i am goroutine_in_2")
            go func() {
                println("i am goroutine_in_3")
                panic("i am panic")
            }()
        }()
    }()

    time.Sleep(1*time.Second);
    println("main goroutine is over")
}

image.png

最后一句,main goroutine is over沒有打印蚀同,程序沒有執(zhí)行到缅刽。 前面我們說到了啊掏。不管你是什么樣的程序,遇到panic 我就終止程序往下執(zhí)行衰猛,哪怕是子協(xié)程呢迟蜜!好了,協(xié)程先說到這里啡省。我們繼續(xù)往下看recover

recover

recover 可以中止panic造成的程序崩潰娜睛,它是一個只能在defer中發(fā)揮作用的函數(shù)。在其他作用域中不會發(fā)揮作用卦睹。為什么這么說呢畦戒?我們看下面這個栗子

package main

import "fmt"

func main() {
    defer func() {
        println("i am main")
    }()
    if err := recover();err != nil {
        fmt.Println(err)
    }
    panic("i am panic")
}


看一下執(zhí)行結(jié)果

image.png

我們看到,遇到panic,執(zhí)行了defer,然后執(zhí)行了panic ,并沒有執(zhí)行if條件判斷结序,為什么障斋?recover是捕捉錯誤的,運行到if 還沒有錯誤徐鹤,捕捉什么垃环?運行到panic 的時候 if 已經(jīng)執(zhí)行過了,怎么捕捉返敬?那么可能有人想晴裹,我把if放到panic后面不就行了嗎?行嗎救赐?答案是否定的涧团,panic 我們前面已經(jīng)說過了,甭管你是誰经磅,看見我就得停止泌绣。那就回到我們剛才說的,panic 出現(xiàn)预厌,程序停止往下執(zhí)行阿迈,但是程序會循環(huán)執(zhí)行defer啊,那如果我在defer里面捕捉錯誤轧叽,是不是就可以解決這個問題了呢苗沧。可見go的設(shè)計者是用心良苦炭晒!到這里有沒有人會問一個問題defer可以嵌套待逞,那么panic能否嵌套呢?當(dāng)然可以网严,defer可以容納一切识樱,panic放到defer里面一樣可以嵌套

package main

func main() {
    defer func() {
        defer func() {
            defer func() {
                panic("i am panic3")
            }()
            panic("i am panic2")
        }()
        panic("i am panic1")
    }()

    panic("i am panic")
}
image.png

為什么會先執(zhí)行 最后一行panic ,才執(zhí)行defer呢,這和前面說的遇到panic先執(zhí)行defer有點出入是吧怜庸,但是你這樣看 defer優(yōu)先于panic優(yōu)先于defer+panic当犯。

那么現(xiàn)在,我們來寫一個例子割疾,看defer 如何捕捉panic并恢復(fù)程序正常執(zhí)行

package main

import "fmt"

func main() {
    havePanic();
    println("i will go on ")
}

func havePanic()  {
    defer func() {
        if err:=recover();err !=nil {
            fmt.Println("i get a panic")
            fmt.Println(err)
        }
    }()
    panic("i am panic");
}

解讀一下上面的程序嚎卫,執(zhí)行havePanic ,havePanic的第一個defer入棧宏榕,往下執(zhí)行碰到panic,首先會執(zhí)行defer拓诸,defer里面打印了err信息,并可以做一些其他的處理担扑,比如記錄日志,重試什么的趣钱。然后main繼續(xù)執(zhí)行下面的print,看一下執(zhí)行結(jié)果

image.png

下面再補充一點協(xié)程的知識

go不是號稱百萬協(xié)程嗎涌献?那么我們真給它來個百萬協(xié)程看一下我的電腦到底能不能hold住

來!寫一段代碼

package main

import (
    "fmt"
    "time"
)

func main()  {
    i :=1;
    for  {
        go func() {
            for  {
                time.Sleep(time.Second)
            }
        }()
        if i > 1000000 {
            fmt.Printf("我已經(jīng)啟動了%d個協(xié)程\n",i)
        }else{
            fmt.Printf("當(dāng)前是第%d個協(xié)程\n",i)
        }
        i++
    }

}

截圖看一下我當(dāng)前的機器狀態(tài)

image.png

百萬協(xié)程掛起之后的截圖

image.png

因為輸出跟不上速度其實最后跑了1842504個協(xié)程

image.png

說一下跑后感:風(fēng)扇呼呼的轉(zhuǎn)了大概三分鐘的樣子
首有,我算了一下一個協(xié)程大概是2.45kb的樣子

image.png

協(xié)程和線程的區(qū)別

一個進程內(nèi)部可以運行多個線程燕垃,而每個線程又可以運行很多協(xié)程。線程要負責(zé)對協(xié)程進行調(diào)度井联,保證每個協(xié)程都有機會得到執(zhí)行卜壕。當(dāng)一個協(xié)程睡眠時,它要將線程的運行權(quán)讓給其它的協(xié)程來運行烙常,而不能持續(xù)霸占這個線程轴捎。同一個線程內(nèi)部最多只會有一個協(xié)程正在運行。

線程的調(diào)度是由操作系統(tǒng)負責(zé)的蚕脏,調(diào)度算法運行在內(nèi)核態(tài)侦副,而協(xié)程的調(diào)用是由 Go 語言的運行時負責(zé)的,調(diào)度算法運行在用戶態(tài)驼鞭。

協(xié)程可以簡化為三個狀態(tài)秦驯,運行態(tài)、就緒態(tài)和休眠態(tài)挣棕。同一個線程中最多只會存在一個處于運行態(tài)的協(xié)程译隘,就緒態(tài)的協(xié)程是指那些具備了運行能力但是還沒有得到運行機會的協(xié)程,它們隨時會被調(diào)度到運行態(tài)洛心,休眠態(tài)的協(xié)程還不具備運行能力固耘,它們是在等待某些條件的發(fā)生,比如 IO 操作的完成词身、睡眠時間的結(jié)束等玻驻。

操作系統(tǒng)對線程的調(diào)度是搶占式的,也就是說單個線程的死循環(huán)不會影響其它線程的執(zhí)行,每個線程的連續(xù)運行受到時間片的限制璧瞬。

Go 語言運行時對協(xié)程的調(diào)度并不是搶占式的户辫。如果單個協(xié)程通過死循環(huán)霸占了線程的執(zhí)行權(quán),那這個線程就沒有機會去運行其它協(xié)程了嗤锉,你可以說這個線程假死了渔欢。不過一個進程內(nèi)部往往有多個線程,假死了一個線程沒事瘟忱,全部假死了才會導(dǎo)致整個進程卡死奥额。

每個線程都會包含多個就緒態(tài)的協(xié)程形成了一個就緒隊列,如果這個線程因為某個別協(xié)程死循環(huán)導(dǎo)致假死访诱,那這個隊列上所有的就緒態(tài)協(xié)程是不是就沒有機會得到運行了呢垫挨?Go 語言運行時調(diào)度器采用了 work-stealing 算法,當(dāng)某個線程空閑時触菜,也就是該線程上所有的協(xié)程都在休眠(或者一個協(xié)程都沒有)九榔,它就會去其它線程的就緒隊列上去偷一些協(xié)程來運行。也就是說這些線程會主動找活干涡相,在正常情況下哲泊,運行時會盡量平均分配工作任務(wù)。

我的線程數(shù)到底有多少催蝗?

默認情況下切威,Go 運行時會將線程數(shù)會被設(shè)置為機器 CPU 邏輯核心數(shù)。同時它內(nèi)置的 runtime 包提供了 GOMAXPROCS(n int) 函數(shù)允許我們動態(tài)調(diào)整線程數(shù)丙号,注意這個函數(shù)名字是全大寫先朦。該函數(shù)會返回修改前的線程數(shù),如果參數(shù) n <=0 犬缨,就不會產(chǎn)生修改效果烙无,等價于讀操作。

package main

import (
    "fmt"
    "runtime"
)

func main()  {
    fmt.Print(runtime.GOMAXPROCS(0))//獲取默認線程數(shù) 8
    println("\n")
    runtime.GOMAXPROCS(10)//設(shè)置線程數(shù)為10
    fmt.Print(runtime.GOMAXPROCS(0))//獲取新線程數(shù) 10
}

image.png

更多文章請微信搜索公眾號<老A技術(shù)聯(lián)盟>或訪問博主網(wǎng)站易查網(wǎng)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末遍尺,一起剝皮案震驚了整個濱河市截酷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乾戏,老刑警劉巖迂苛,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鼓择,居然都是意外死亡三幻,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門呐能,熙熙樓的掌柜王于貴愁眉苦臉地迎上來念搬,“玉大人抑堡,你說我怎么就攤上這事±驶玻” “怎么了首妖?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爷恳。 經(jīng)常有香客問我有缆,道長,這世上最難降的妖魔是什么温亲? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任棚壁,我火速辦了婚禮,結(jié)果婚禮上栈虚,老公的妹妹穿的比我還像新娘袖外。我一直安慰自己,他們只是感情好魂务,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布曼验。 她就那樣靜靜地躺著,像睡著了一般头镊。 火紅的嫁衣襯著肌膚如雪蚣驼。 梳的紋絲不亂的頭發(fā)上魄幕,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天相艇,我揣著相機與錄音,去河邊找鬼纯陨。 笑死坛芽,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翼抠。 我是一名探鬼主播咙轩,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阴颖!你這毒婦竟也來了活喊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤量愧,失蹤者是張志新(化名)和其女友劉穎钾菊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偎肃,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡煞烫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了累颂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片滞详。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出料饥,到底是詐尸還是另有隱情蒲犬,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布稀火,位于F島的核電站暖哨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏凰狞。R本人自食惡果不足惜篇裁,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赡若。 院中可真熱鬧达布,春花似錦、人聲如沸逾冬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽身腻。三九已至产还,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嘀趟,已是汗流浹背脐区。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留她按,地道東北人牛隅。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓躏尉,卻偏偏與公主長得像羡亩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子谴仙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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