Go實踐:Goroutine(go協(xié)程)調(diào)度原理及應(yīng)用

什么是協(xié)程?

進程和線程

一個應(yīng)用程序時運行在操作系統(tǒng)上的一個進程阱表。進程是一個運行在自己獨立內(nèi)存空間的獨立執(zhí)行體殿如,是操作系統(tǒng)進行資源分配的最小單位。一個進程則有一個或多個線程組成最爬,這些線程是共享進程內(nèi)存地址空間的執(zhí)行體涉馁,是操作系統(tǒng)進行任務(wù)調(diào)度的最小單位。而使用多線程進行工作時爱致,由于共享父進程的內(nèi)存空間等資源烤送,訪問同一個數(shù)據(jù)需要對其進行加鎖,保證同一時間只有一個線程操作一個數(shù)據(jù)糠悯。這樣不僅會提高編碼的復雜度帮坚,還會有多個線程搶占鎖、線程切換帶來的額外開銷互艾。

協(xié)程

在Go中试和,應(yīng)用程序并發(fā)處理的部分被稱作goroutines(協(xié)程),它是一種更輕量級的線程纫普,和操作系統(tǒng)的線程之間并無一對一的關(guān)系阅悍。協(xié)程是根據(jù)一個或多個線程的可用性,映射(多路復用局嘁,執(zhí)行于)在它們之上的溉箕;協(xié)程調(diào)度器負責在Go運行時調(diào)度進行協(xié)程的工作。

通道(Channel)

協(xié)程工作在相同的地址空間中悦昵,所以共享內(nèi)存的方式是同步的,可以使用互斥鎖來實現(xiàn)晌畅,但是Go中更好的方案是使用Channel來同步協(xié)程但指。
通道類型(Chan)就像一個可用于發(fā)送類型化數(shù)據(jù)的管道,由其負責協(xié)程之間的通信,在任何時間棋凳,一個通道數(shù)據(jù)被設(shè)計為只有一個協(xié)程可以對其訪問拦坠,所以不會發(fā)生數(shù)據(jù)競爭。

通道阻塞

默認情況下剩岳,Go創(chuàng)建的通道是同步且無緩沖的:在有接受者接受數(shù)據(jù)之前贞滨,發(fā)送不會結(jié)束,發(fā)送者是直接將數(shù)據(jù)交給接受者的拍棕,所以這種通道的發(fā)送或接受操作在對方準備好之前都是阻塞的晓铆。
例如以下代碼,執(zhí)行會報錯死鎖:
示例1.1:

func main() {
    ch := make(chan int)
    ch <- 1
    <-ch
}

因為對ch的讀寫都在main函數(shù)的主協(xié)程中绰播,執(zhí)行到ch <-1時由于接收ch的數(shù)據(jù)還沒準備好骄噪,發(fā)送數(shù)據(jù)將被阻塞,程序無法繼續(xù)執(zhí)行蠢箩。必須使用關(guān)鍵字go來啟動一個新的協(xié)程發(fā)送數(shù)據(jù)链蕊,另一個協(xié)程接收數(shù)據(jù),如下所示:
示例1.2

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
    }()
    fmt.Println(<- ch)
}

使用make創(chuàng)建一個通道的時候可以傳入第二個參數(shù)指定通道緩沖區(qū)大小谬泌,這種方式在通道寫滿之前滔韵,發(fā)送數(shù)據(jù)不會被阻塞,通道不為空時接收操作不會被阻塞掌实,如果將示例1.1中的創(chuàng)建通道傳第二個參數(shù)為1奏属,就可以正常運行不會死鎖了,代碼如下:
示例1.3

func main() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println(<- ch)
}

Go協(xié)程調(diào)度原理

調(diào)度器架構(gòu)

Go的調(diào)度器從最開始的單線程經(jīng)過不斷的改進潮峦、優(yōu)化囱皿,發(fā)展到現(xiàn)在的GMP模型,在GMP模型中有三個重要的結(jié)構(gòu):

  • G(Goroutine):go協(xié)程忱嘹,一個可執(zhí)行單元嘱腥,調(diào)度器作用就是對所有G的切換
  • M(Thread):操作系統(tǒng)上的線程,G運行與M上拘悦,一個G可能由多個不同的M運行齿兔,一個M可以運行多個G
  • P(Processor):處理器,他包含了運行G的資源础米,如果線程M想運行G分苇,必須先獲取P,P還包含了可運行的G隊列屁桑。一個M一個時刻只擁有一個P医寿,M和P的數(shù)量是1:1的。
GMP模型架構(gòu)

上圖中各個模塊的作用如下:

  1. 全局隊列:存放等待運行G
  2. P的本地隊列:和全局隊列類似蘑斧,存放的也是等待運行的G靖秩,存放數(shù)量上限256個须眷。新建G時,G優(yōu)先加入到P的本地隊列沟突,如果隊列滿了花颗,則會把本地隊列中的一半G移動到全局隊列
  3. P列表:所有的P都在程序啟動時創(chuàng)建,保存在數(shù)組中惠拭,最多有GOMAXPROCS個扩劝,可通過runtime.GOMAXPROCS(N)修改,N表示設(shè)置的個數(shù)

M是Goroutine調(diào)度器和操作系統(tǒng)調(diào)度器的橋梁职辅,每個M代表一個內(nèi)核線程棒呛,操作系統(tǒng)調(diào)度器負責把內(nèi)核線程分配到CPU的核心上執(zhí)行。

調(diào)度策略

復用線程

調(diào)度器核心思想是盡可能避免頻繁的創(chuàng)建罐农、銷毀線程条霜,對線程進行復用以提高效率。
1. work stealing機制(竊取式)
當本線程無G可運行時涵亏,嘗試從其他線程綁定的P竊取G宰睡,而不是直接銷毀線程。
2. hand off機制
當本線程M因為G進行的系統(tǒng)調(diào)用阻塞是气筋,線程釋放綁定的P拆内,把P轉(zhuǎn)移給其他空閑的M'執(zhí)行。

利用多核CPU并行

GOMAXPROCS設(shè)置P的數(shù)量宠默,最多有GOMAXPROCS個線程分布在多個CPU核心上運行麸恍。

搶占

一個goroutine最多占用CPU10ms,防止其他goroutine等待太久得不到執(zhí)行被“餓死”搀矫。

全局G隊列

全局G隊列是有互斥鎖保護的抹沪,訪問需要競爭鎖,新的調(diào)度器將其功能弱化了瓤球,當M執(zhí)行work stealing從其他P竊取不到G時融欧,才會去全局G隊列獲取G。

Go并發(fā)編程實例

兩個協(xié)程交替打印1-100

用兩個協(xié)程順序打印出1-100卦羡,要求一個協(xié)程打印1噪馏、3、5绿饵、7...奇數(shù)欠肾,另一個協(xié)程打印2、4拟赊、6刺桃、8...偶數(shù),輸出必須是順序的要门。

示例代碼:

func main() {
    // ch用來同步兩個協(xié)程交替執(zhí)行
    ch := make(chan int)
    // ch_end用來阻塞主程序虏肾,讓兩個協(xié)程可以執(zhí)行完
    ch_end := make(chan int)
    go func() {
        for i := 1; i <= 100; i++ {
            ch <- 1
            if i % 2 == 0 {
                fmt.Println(i)
            }
        }
        ch_end <- 1
    }()
    go func() {
        for i := 1; i <= 100; i++ {
            <-ch
            if i % 2 != 0 {
                fmt.Println(i)
            }
        }
    }()
    <-ch_end
}

并行素數(shù)篩選

有一個協(xié)程不斷生2~n的自然數(shù)廓啊,對每個素數(shù)起一個協(xié)程欢搜,用來篩選素數(shù)

示例代碼:

func generate(ch chan int, n int) {
    for i := 2; i <= n ; i++ {
        fmt.Println("generate:", i)
        ch <- i
    }
    close(ch)
}

func filter(in, out chan int, prime int) {
    for i := range in {
        fmt.Printf("filter(%d): %d\n", prime, i)
        if i % prime != 0 {
            out <- i
        }
    }
    close(out)
}


func main() {
    res := []int{}
    ch := make(chan int)
    go generate(ch, 112)
    
    for {
        if prime, ok := <- ch; ok {
            res = append(res, prime)
            ch_out := make(chan int)
            go filter(ch, ch_out, prime)
            // 前一個素數(shù)過濾協(xié)程的輸出通道是后一個素數(shù)過濾通道的輸入通道
            ch = ch_out
        } else {
            break
        }
    }
    fmt.Println("main:", res)
}

實現(xiàn)超時機制

當設(shè)置的超時時間到達后如果work還不可執(zhí)行就終止等待封豪,返回超時

示例代碼

func TimeOut(timeout time.Duration) {
    ch_to := make(chan bool, 1)
    go func() {
        time.Sleep(timeout)
        ch_to <- true
    }()

    ch_do := make(chan int, 1)
    go func() {
        time.Sleep(3e9)
        ch_do <- 1
    }()

    select {
    case i := <- ch_do:
        fmt.Println("do something, id:", i)
    case <-ch_to:
        fmt.Println("timeout")
        break
    }
}

實現(xiàn)迭代器

實現(xiàn)一個惰性迭代器,每次調(diào)用返回一個列表元素炒瘟,直到所有的元素被訪問完返回nil

示例代碼:

// 迭代器
func iterator(iterable []interface{}) chan interface{}{
    yield := make(chan interface{})
    go func() {
        for i := 0; i < len(iterable); i++ {
            yield <- iterable[i]
        }
        close(yield)
    }()
    return yield
}

// 獲取下一個元素
func next(iter chan interface{}) interface{} {
    for v := range iter {
        return v
    }
    return nil
}

func main() {
    nums := []interface{}{1, 2, 3, 4, 5}
    iter := iterator(nums)
    for v := next(iter); v != nil; v = next(iter) {
        fmt.Println(v)
    }
}

參考

【1】《The Way to Go》:并發(fā)吹埠、并行和協(xié)程
【2】Golang的協(xié)程調(diào)度器原理及GMP設(shè)計思想?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疮装,一起剝皮案震驚了整個濱河市缘琅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌廓推,老刑警劉巖刷袍,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異樊展,居然都是意外死亡呻纹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進店門专缠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來雷酪,“玉大人,你說我怎么就攤上這事涝婉「缌Γ” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵墩弯,是天一觀的道長吩跋。 經(jīng)常有香客問我,道長渔工,這世上最難降的妖魔是什么锌钮? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮涨缚,結(jié)果婚禮上轧粟,老公的妹妹穿的比我還像新娘。我一直安慰自己脓魏,他們只是感情好兰吟,可當我...
    茶點故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茂翔,像睡著了一般混蔼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上珊燎,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天惭嚣,我揣著相機與錄音遵湖,去河邊找鬼。 笑死晚吞,一個胖子當著我的面吹牛延旧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播槽地,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼迁沫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捌蚊?” 一聲冷哼從身側(cè)響起集畅,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缅糟,沒想到半個月后挺智,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡窗宦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年赦颇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迫摔。...
    茶點故事閱讀 39,769評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡沐扳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出句占,到底是詐尸還是另有隱情沪摄,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布纱烘,位于F島的核電站杨拐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏擂啥。R本人自食惡果不足惜哄陶,卻給世界環(huán)境...
    茶點故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哺壶。 院中可真熱鬧屋吨,春花似錦、人聲如沸山宾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽资锰。三九已至敢课,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背直秆。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工濒募, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人圾结。 一個月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓瑰剃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疫稿。 傳聞我的和親對象是個殘疾皇子培他,可洞房花燭夜當晚...
    茶點故事閱讀 44,678評論 2 354

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