Go并發(fā)編程之并發(fā)和Goroutine

概述

簡而言之,所謂并發(fā)編程是指在一臺處理器上“同時”處理多個任務(wù)怯晕。

隨著硬件的發(fā)展缸棵,并發(fā)程序變得越來越重要。Web服務(wù)器會一次處理成千上萬的請求吧凉。平板電腦和手機app在渲染用戶畫面同時還會后臺執(zhí)行各種計算任務(wù)和網(wǎng)絡(luò)請求阀捅。即使是傳統(tǒng)的批處理問題--讀取數(shù)據(jù),計算凄诞,寫輸出--現(xiàn)在也會用并發(fā)來隱藏掉I/O的操作延遲以充分利用現(xiàn)代計算機設(shè)備的多個核心帆谍。計算機的性能每年都在以非線性的速度增長轴咱。

宏觀的并發(fā)是指在一段時間內(nèi),有多個程序在同時運行窖剑。

并發(fā)在微觀上,是指在同一時刻只能有一條指令執(zhí)行酪术,但多個程序指令被快速的輪換執(zhí)行绘雁,使得在宏觀上具有多個進程同時執(zhí)行的效果,但在微觀上并不是同時執(zhí)行的欣除,只是把時間分成若干段挪略,使多個程序快速交替的執(zhí)行杠娱。

并行和并發(fā)

并行(parallel): 指在同一時刻,有多條指令在多個處理器上同時執(zhí)行禽拔。

并發(fā)(concurrency): 指在同一時刻只能有一條指令執(zhí)行睹栖,但多個進程指令被快速的輪換執(zhí)行,使得在宏觀上具有多個進程同時執(zhí)行的效果恼除,但在微觀上并不是同時執(zhí)行的梁只,只是把時間分成若干段搪锣,通過cpu時間片輪轉(zhuǎn)使多個進程快速交替的執(zhí)行。

常見并發(fā)編程技術(shù)

1. 進程并發(fā)

  • 程序和進程
    • 程序灰追,是指編譯好的二進制文件弹澎,在磁盤上努咐,不占用系統(tǒng)資源(cpu、內(nèi)存佩迟、打開的文件报强、設(shè)備拱燃、鎖....)
    • 進程碗誉,是一個抽象的概念,與操作系統(tǒng)原理聯(lián)系緊密苍蔬。進程是活躍的程序蝴蜓,占用系統(tǒng)資源。在內(nèi)存中執(zhí)行格仲。(程序運行起來凯肋,產(chǎn)生一個進程)
  • 進程狀態(tài)
    進程基本的狀態(tài)有5種侮东。分別為初始態(tài),就緒態(tài)驱敲,運行態(tài)宽闲,掛起態(tài)與終止態(tài)容诬。其中初始態(tài)為進程準(zhǔn)備階段,常與就緒態(tài)結(jié)合來看狈定。
進程狀態(tài)流程圖.png
  • 進程并發(fā)
    在使用進程 實現(xiàn)并發(fā)時會出現(xiàn)什么問題呢纽什?

    1. 系統(tǒng)開銷比較大稿湿,占用資源比較多押赊,開啟進程數(shù)量比較少流礁。
    2. 在unix/linux系統(tǒng)下罗丰,還會產(chǎn)生“孤兒進程”和“僵尸進程”萌抵。

    通過前面查看操作系統(tǒng)的進程信息元镀,我們知道在操作系統(tǒng)中栖疑,可以產(chǎn)生很多的進程滔驶。在unix/linux系統(tǒng)中揭糕,正常情況下,子進程是通過父進程fork創(chuàng)建的揪漩,子進程再創(chuàng)建新的進程氢拥。

    并且父進程永遠無法預(yù)測子進程 到底什么時候結(jié)束锨侯。 當(dāng)一個 進程完成它的工作終止之后囚痴,它的父進程需要調(diào)用系統(tǒng)調(diào)用取得子進程的終止?fàn)顟B(tài)。

    孤兒進程:父進程先于子進程結(jié)束奕谭,則子進程成為孤兒進程痴荐,子進程的父進程成為init進程生兆,稱為init進程領(lǐng)養(yǎng)孤兒進程鸦难。

    僵尸進程: 進程終止,父進程尚未回收击敌,子進程殘留資源(PCB)存放于內(nèi)核中沃斤,變成僵尸(Zombie)進程。

    Windows下的進程和Linux下的進程是不一樣的捅彻,它比較懶惰鞍陨,從來不執(zhí)行任何東西诚撵,只是為線程提供執(zhí)行環(huán)境寿烟。然后由線程負責(zé)執(zhí)行包含在進程的地址空間中的代碼。當(dāng)創(chuàng)建一個進程的時候缝其,操作系統(tǒng)會自動創(chuàng)建這個進程的第一個線程内边,成為主線程待锈。

2. 線程并發(fā)

  • 什么是線程?
    LWP:light weight process 輕量級的進程竿音,本質(zhì)仍是進程 (Linux下)
    進程:獨立地址空間春瞬,擁有PCB,最小分配資源單位随常,可看成是只有一個線程的進程抹竹。
    線程: 最小的執(zhí)行單位窃判,有獨立的PCB袄琳,但沒有獨立的地址空間(共享)
    區(qū)別:在于是否共享地址空間。獨居(進程)宛琅;合租(線程)嘿辟。

    進程和線程.png

    Windows系統(tǒng)下红伦,可以直接忽略進程的概念昙读,只談線程膨桥。因為線程是最小的執(zhí)行單位只嚣,是被系統(tǒng)獨立調(diào)度和分派的基本單位。而進程只是給線程提供執(zhí)行環(huán)境壮虫。

  • 線程同步
    同步即協(xié)同步調(diào)囚似,按預(yù)定的先后次序運行饶唤。

    線程同步贯钩,指一個線程發(fā)出某一功能調(diào)用時角雷,在沒有得到結(jié)果之前,該調(diào)用不返回雷滚。同時其它線程為保證數(shù)據(jù)一致性祈远,不能調(diào)用該功能车份。

    線程不同步產(chǎn)生的現(xiàn)象叫做“與時間有關(guān)的錯誤”(time related)。為了避免這種數(shù)據(jù)混亂出爹,線程需要同步以政。

    “同步”的目的,是為了避免數(shù)據(jù)混亂伴找,解決與時間有關(guān)的錯誤盈蛮。實際上,不僅線程間需要同步技矮,進程間抖誉、信號間等等都需要同步機制。

    因此衰倦,所有“多個控制流袒炉,共同操作一個共享資源”的情況,都需要同步樊零。

  • 互斥量mutex

    Linux中提供一把互斥鎖mutex(也稱之為互斥量)。

    每個線程在對資源操作前都嘗試先加鎖驻襟,成功加鎖才能操作夺艰,操作結(jié)束解鎖。

    資源還是共享的沉衣,線程間也還是競爭的郁副,但通過“鎖”就將資源的訪問變成互斥操作,而后與時間有關(guān)的錯誤也不會再產(chǎn)生了豌习。


    同步鎖.png

    但存谎,應(yīng)注意:同一時刻,只能有一個線程持有該鎖肥隆。

    當(dāng)A線程對某個全局變量加鎖訪問既荚,B在訪問前嘗試加鎖,拿不到鎖栋艳,B阻塞恰聘。C線程不去加鎖,而直接訪問該全局變量,依然能夠訪問憨琳,但會出現(xiàn)數(shù)據(jù)混亂。

    所以旬昭,互斥鎖實質(zhì)上是操作系統(tǒng)提供的一把“建議鎖”(又稱“協(xié)同鎖”)篙螟,建議程序中有多線程訪問共享資源的時候使用該機制。但问拘,并沒有強制限定遍略。

    因此,即使有了mutex骤坐,如果有線程不按規(guī)則來訪問數(shù)據(jù)绪杏,依然會造成數(shù)據(jù)混亂。

  • 讀寫鎖
    與互斥量類似纽绍,但讀寫鎖允許更高的并行性蕾久。其特性為:寫?yīng)氄迹x共享拌夏。

    讀寫鎖狀態(tài):
    特別強調(diào):讀寫鎖只有一把僧著,但其具備兩種狀態(tài):

    1. 讀模式下加鎖狀態(tài) (讀鎖)
    2. 寫模式下加鎖狀態(tài) (寫鎖)

    讀寫鎖特性:

    1. 讀寫鎖是“寫模式加鎖”時, 解鎖前障簿,所有對該鎖加鎖的線程都會被阻塞盹愚。
    2. 讀寫鎖是“讀模式加鎖”時, 如果線程以讀模式對其加鎖會成功站故;如果線程以寫模式加鎖會阻塞皆怕。
    3. 讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的線程西篓,也有試圖以讀模式加鎖的線程愈腾。那么讀寫鎖會阻塞隨后的讀模式鎖請求。優(yōu)先滿足寫模式鎖岂津。讀鎖顶滩、寫鎖并行阻塞,寫鎖優(yōu)先級高寸爆。

    讀寫鎖也叫共享-獨占鎖礁鲁。當(dāng)讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的赁豆;當(dāng)它以寫模式鎖住時仅醇,它是以獨占模式鎖住的。寫?yīng)氄寄е帧⒆x共享析二。

    讀寫鎖非常適合于對數(shù)據(jù)結(jié)構(gòu)讀的次數(shù)遠大于寫的情況。

3. 協(xié)程并發(fā)

協(xié)程:coroutine。也叫輕量級線程叶摄。

與傳統(tǒng)的系統(tǒng)級線程和進程相比属韧,協(xié)程最大的優(yōu)勢在于“輕量級”「蛳牛可以輕松創(chuàng)建上萬個而不會導(dǎo)致系統(tǒng)資源衰竭宵喂。而線程和進程通常很難超過1萬個。這也是協(xié)程別稱“輕量級線程”的原因会傲。

一個線程中可以有任意多個協(xié)程锅棕,但某一時刻只能有一個協(xié)程在運行,多個協(xié)程分享該線程分配到的計算機資源淌山。

多數(shù)語言在語法層面并不直接支持協(xié)程裸燎,而是通過庫的方式支持,但用庫的方式支持的功能也并不完整泼疑,比如僅僅提供協(xié)程的創(chuàng)建德绿、銷毀與切換等能力。如果在這樣的輕量級線程中調(diào)用一個同步 IO 操作退渗,比如網(wǎng)絡(luò)通信脆炎、本地文件讀寫,都會阻塞其他的并發(fā)執(zhí)行輕量級線程氓辣,從而無法真正達到輕量級線程本身期望達到的目標(biāo)秒裕。

在協(xié)程中,調(diào)用一個任務(wù)就像調(diào)用一個函數(shù)一樣钞啸,消耗的系統(tǒng)資源最少几蜻!但能達到進程、線程并發(fā)相同的效果体斩。

在一次并發(fā)任務(wù)中梭稚,進程、線程絮吵、協(xié)程均可以實現(xiàn)弧烤。從系統(tǒng)資源消耗的角度出發(fā)來看,進程相當(dāng)多蹬敲,線程次之暇昂,協(xié)程最少。

Go并發(fā)

1. 什么是Goroutine

goroutine是Go并行設(shè)計的核心伴嗡。goroutine說到底其實就是協(xié)程急波,它比線程更小,十幾個goroutine可能體現(xiàn)在底層就是五六個線程瘪校,Go語言內(nèi)部幫你實現(xiàn)了這些goroutine之間的內(nèi)存共享澄暮。執(zhí)行g(shù)oroutine只需極少的棧內(nèi)存(大概是4~5KB)名段,當(dāng)然會根據(jù)相應(yīng)的數(shù)據(jù)伸縮。也正因為如此泣懊,可同時運行成千上萬個并發(fā)任務(wù)伸辟。goroutine比thread更易用、更高效馍刮、更輕便信夫。

一般情況下,一個普通計算機跑幾十個線程就有點負載過大了渠退,但是同樣的機器卻可以輕松地讓成百上千個goroutine進行資源競爭忙迁。

2. Goroutine的創(chuàng)建

只需在函數(shù)調(diào)?語句前添加 go 關(guān)鍵字脐彩,就可創(chuàng)建并發(fā)執(zhí)?單元碎乃。開發(fā)?員無需了解任何執(zhí)?細節(jié),調(diào)度器會自動將其安排到合適的系統(tǒng)線程上執(zhí)行惠奸。

在并發(fā)編程中梅誓,我們通常想將一個過程切分成幾塊,然后讓每個goroutine各自負責(zé)一塊工作佛南,當(dāng)一個程序啟動時梗掰,主函數(shù)在一個單獨的goroutine中運行,我們叫它main goroutine嗅回。新的goroutine會用go語句來創(chuàng)建及穗。而go語言的并發(fā)設(shè)計,讓我們很輕松就可以達成這一目的绵载。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

func main() {
    //創(chuàng)建一個 goroutine埂陆,啟動另外一個任務(wù)
    go newTask()
    i := 0
    //main goroutine 循環(huán)打印
    for {
        i++
        fmt.Printf("main goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

3. Goroutine特性

主goroutine退出后,其它的工作goroutine也會自動退出:

package main

import (
"fmt"
"time"
)

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

func main() {
    //創(chuàng)建一個 goroutine娃豹,啟動另外一個任務(wù)
    go newTask()

    fmt.Println("main goroutine exit")
}

4. runtime包

  • Gosched
    runtime.Gosched() 用于讓出CPU時間片焚虱,讓出當(dāng)前goroutine的執(zhí)行權(quán)限,調(diào)度器安排其他等待的任務(wù)運行懂版,并在下次再獲得cpu時間輪片的時候鹃栽,從該出讓cpu的位置恢復(fù)執(zhí)行。

    有點像跑接力賽躯畴,A跑了一會碰到代碼runtime.Gosched() 就把接力棒交給B了民鼓,A歇著了,B繼續(xù)跑蓬抄。

    示例代碼:

package main

import (
"fmt"
"runtime"
)

func main() {
    //創(chuàng)建一個goroutine
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")

    for i := 0; i < 2; i++ {
        runtime.Gosched()  //import "runtime" 包
        /*
            屏蔽runtime.Gosched()運行結(jié)果如下:
                hello
                hello

            沒有runtime.Gosched()運行結(jié)果如下:
                world
                world
                hello
                hello
        */
        fmt.Println("hello")
    }
}

以上程序的執(zhí)行過程如下:
主協(xié)程進入main()函數(shù)摹察,進行代碼的執(zhí)行。當(dāng)執(zhí)行到go func()匿名函數(shù)時倡鲸,創(chuàng)建一個新的協(xié)程供嚎,開始執(zhí)行匿名函數(shù)中的代碼,主協(xié)程繼續(xù)向下執(zhí)行,執(zhí)行到runtime.Gosched( )時會暫停向下執(zhí)行克滴,直到其它協(xié)程執(zhí)行完后逼争,再回到該位置,主協(xié)程繼續(xù)向下執(zhí)行劝赔。

  • Goexit
    調(diào)用 runtime.Goexit() 將立即終止當(dāng)前 goroutine 執(zhí)?誓焦,調(diào)度器確保所有已注冊 defer延遲調(diào)用被執(zhí)行。

    示例代碼:

package main

import (
"fmt"
"runtime"
)

func main() {
   go func() {
       defer fmt.Println("A.defer")

       func() {
           defer fmt.Println("B.defer")
           runtime.Goexit() // 終止當(dāng)前 goroutine, import "runtime"
           fmt.Println("B") // 不會執(zhí)行
       }()

       fmt.Println("A") // 不會執(zhí)行
   }()     //不要忘記()

   //死循環(huán)着帽,目的不讓主goroutine結(jié)束
   for {
   }
}
  • GOMAXPROCS
    調(diào)用 runtime.GOMAXPROCS() 用來設(shè)置可以并行計算的CPU核數(shù)的最大值杂伟,并返回之前的值。

    示例代碼:

package main

import (
    "fmt"
)

func main() {
//n := runtime.GOMAXPROCS(1)    // 第一次 測試
//打印結(jié)果:111111111111111111110000000000000000000011111...

n := runtime.GOMAXPROCS(2)         // 第二次 測試
//打印結(jié)果:010101010101010101011001100101011010010100110...
    fmt.Printf("n = %d\n", n)

    for {
        go fmt.Print(0)
        fmt.Print(1)
    }
}

在第一次執(zhí)行runtime.GOMAXPROCS(1) 時仍翰,最多同時只能有一個goroutine被執(zhí)行赫粥。所以會打印很多1。過了一段時間后予借,GO調(diào)度器會將其置為休眠越平,并喚醒另一個goroutine,這時候就開始打印很多0了灵迫,在打印的時候秦叛,goroutine是被調(diào)度到操作系統(tǒng)線程上的。

在第二次執(zhí)行runtime.GOMAXPROCS(2) 時瀑粥, 我們使用了兩個CPU挣跋,所以兩個goroutine可以一起被執(zhí)行,以同樣的頻率交替打印0和1狞换。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末避咆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子哀澈,更是在濱河造成了極大的恐慌牌借,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件割按,死亡現(xiàn)場離奇詭異膨报,居然都是意外死亡,警方通過查閱死者的電腦和手機适荣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門现柠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弛矛,你說我怎么就攤上這事够吩。” “怎么了丈氓?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵周循,是天一觀的道長强法。 經(jīng)常有香客問我,道長湾笛,這世上最難降的妖魔是什么饮怯? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮嚎研,結(jié)果婚禮上蓖墅,老公的妹妹穿的比我還像新娘。我一直安慰自己临扮,他們只是感情好论矾,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杆勇,像睡著了一般贪壳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上靶橱,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天寥袭,我揣著相機與錄音路捧,去河邊找鬼关霸。 笑死,一個胖子當(dāng)著我的面吹牛杰扫,可吹牛的內(nèi)容都是我干的队寇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼章姓,長吁一口氣:“原來是場噩夢啊……” “哼佳遣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凡伊,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤零渐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后系忙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诵盼,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年银还,在試婚紗的時候發(fā)現(xiàn)自己被綠了风宁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛹疯,死狀恐怖戒财,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捺弦,我是刑警寧澤饮寞,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布孝扛,位于F島的核電站,受9級特大地震影響幽崩,放射性物質(zhì)發(fā)生泄漏疗琉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一歉铝、第九天 我趴在偏房一處隱蔽的房頂上張望盈简。 院中可真熱鬧,春花似錦太示、人聲如沸柠贤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臼勉。三九已至,卻和暖如春餐弱,著一層夾襖步出監(jiān)牢的瞬間宴霸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工膏蚓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瓢谢,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓驮瞧,卻偏偏與公主長得像氓扛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子论笔,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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