golang并發(fā)總結(jié)

golang并發(fā)模型

  • go在語(yǔ)言層面提供了內(nèi)置的并發(fā)支持
  • 不要通過(guò)共享內(nèi)存來(lái)通信,而應(yīng)該通過(guò)通信來(lái)共享內(nèi)存


并發(fā)與并行

  • 定義
    • 并發(fā): 指同一時(shí)刻, 系統(tǒng)通過(guò)調(diào)度,來(lái)回切換交替的運(yùn)行多個(gè)任務(wù),看起來(lái)是"同時(shí)"進(jìn)行的.一個(gè)處理器同時(shí)處理多個(gè)任務(wù)
    • 并行:指同一時(shí)刻,2個(gè)任務(wù)"真正的"同時(shí)進(jìn)行.多個(gè)處理器或者多核的處理器同時(shí)處理多個(gè)不同的任務(wù).
  • 并行:多核cpu,物理上的同時(shí)執(zhí)行.
image
  • 并發(fā):同一時(shí)刻,只能一條指令執(zhí)行.通過(guò)快速輪換執(zhí)行,宏觀上是多個(gè)線程同時(shí)執(zhí)行的.微觀上不是同時(shí)執(zhí)行的,只是把時(shí)間分成若干段,使得多個(gè)線程快速交替執(zhí)行.(單核cpu,邏輯上的同時(shí)執(zhí)行)
image

常見(jiàn)的并發(fā)編程模型

  • 進(jìn)程&線程(Apache)
最初的web服務(wù)器都是基于進(jìn)程和線程,比如Apache,新到的一個(gè)請(qǐng)求就會(huì)分配一個(gè)進(jìn)程或者線程,每個(gè)進(jìn)程只服務(wù)一個(gè)用戶,早期的互聯(lián)網(wǎng)還不夠普及,用戶也不夠多,這時(shí)候網(wǎng)站是可以穩(wěn)定的,問(wèn)題是
進(jìn)程很昂貴,一臺(tái)服務(wù)器無(wú)法創(chuàng)建很多的進(jìn)程,后來(lái)隨著互聯(lián)網(wǎng)的發(fā)展,用戶越來(lái)越多,
網(wǎng)站也變得越來(lái)越復(fù)雜,一個(gè)頁(yè)面有可能就有上百個(gè)請(qǐng)求,
所以就誕生了C10K的問(wèn)題,C10K的意思就是服務(wù)器同時(shí)支持一個(gè)10k量級(jí)的并發(fā)連接,就是要?jiǎng)?chuàng)建1w個(gè)進(jìn)程,
這樣的話,操作系統(tǒng)肯定是無(wú)法承受的.
所以進(jìn)程和線程模型就顯得很力不從心了.
  • 異步非阻塞(Nginx)
為了解決c10k的問(wèn)題,就發(fā)明了異步非阻塞這種技術(shù),一個(gè)典型的案例就是linux里的epoll,
一個(gè)普通的服務(wù)器就能服務(wù)大量的用戶,資源消耗也很低,像NGINX都是epoll的產(chǎn)物,
但是,異步非阻塞也不是很完美,為了追求性能,強(qiáng)行的將線性程序打亂,開(kāi)發(fā)和維護(hù)都變得非常復(fù)雜,
調(diào)試起來(lái)也比較困難.
  • 協(xié)程(Golang)
為了降低開(kāi)發(fā)的復(fù)雜度,讓程序員同學(xué)更爽的寫(xiě)代碼,協(xié)程這種并發(fā)模型就逐漸流行起來(lái),
這種模型,可以讓我們像寫(xiě)線性程序一樣,來(lái)寫(xiě)異步的程序,
其實(shí)協(xié)程的底層就是線程,但它比線程更輕量,幾十個(gè)協(xié)程,體現(xiàn)在底層,可能也就是五六個(gè)的線程.
大家把協(xié)程理解成,更高效,更易用,更輕量的線程.

Glang并發(fā)的實(shí)現(xiàn)

  • 程序并發(fā)執(zhí)行(goroutine)
每開(kāi)啟一個(gè)協(xié)程,就會(huì)有一個(gè)goroutine,負(fù)責(zé)程序的并發(fā)執(zhí)行
f1() // 執(zhí)行函數(shù)f1,等待函數(shù)f1返回

go f1() // 執(zhí)行函數(shù)f1
f2()    // 不用等待f1()的返回

使用起來(lái)很簡(jiǎn)單, 只要在一個(gè)函數(shù)前面加 go 關(guān)鍵字就會(huì)創(chuàng)建一個(gè)goroutine, 去并發(fā)的執(zhí)行,
所以程序并不會(huì)阻塞, 最后這2個(gè)函數(shù)相當(dāng)于會(huì)去并發(fā)的執(zhí)行.
有了goroutine之后就可以并發(fā)的去執(zhí)行了,這就引出了一個(gè)問(wèn)題:在多個(gè)goroutine之間是如何進(jìn)行數(shù)據(jù)通信的呢?
比如 go f1() 和 f2() 這2個(gè)函數(shù)之間要通信,要傳遞數(shù)據(jù)的話,那他們是怎么進(jìn)行的?

使用channel在多個(gè)goroutine之間進(jìn)行數(shù)據(jù)通信和同步的
  • 多個(gè)goroutine間的數(shù)據(jù)同步和通信(channel)
    • channel的基礎(chǔ)語(yǔ)法
      • 創(chuàng)建1:make(chan [type]) // 無(wú)緩沖
      • 創(chuàng)建2:make(chan [type], int) // 有緩沖
      • 寫(xiě)入:channel <-
      • 獲取: <- channel
    c := make(chan string) // 聲明一個(gè)無(wú)緩沖的channel
    // 創(chuàng)建一個(gè)goroutine
    go func() {  
        c <- "this is channel msg " // 發(fā)送數(shù)據(jù)到channel里
    }()
    msg := <-c // 阻塞直到接收到數(shù)據(jù)
    fmt.Println(msg)
    
說(shuō)明:
// channel分為無(wú)緩沖信道(即unbuffered channel)和有緩沖信道(buffered channel)。
無(wú)緩沖的與有緩沖channel有著重大差別:一個(gè)是同步的 一個(gè)是非同步的
ch1:=make(chan int)        無(wú)緩沖
ch2:=make(chan int,1)      有緩沖
ch1<-1 // 無(wú)緩沖的
這里要有別的協(xié)程一直<-ch1 接受這個(gè)參數(shù),
那么ch1<-1之后的代碼才能執(zhí)行,要不然就一直阻塞著,
ch2<-1 // 有緩沖的
這里則不會(huì)阻塞,因?yàn)榫彌_大小是1(放了一個(gè)緩沖就剩0了),只有當(dāng)放第二個(gè)的時(shí)候,第一個(gè)還沒(méi)被拿走,這時(shí)候才會(huì)阻塞.
比喻:
    1. 無(wú)緩沖的就是一個(gè)送信人去你家門口送信,你不在家他不走,一定要送到你手里他才走.
    無(wú)緩沖保證信能到你手上.
    2. 有緩沖的就是一個(gè)送信人去你家門口送信,扔到你家信箱轉(zhuǎn)身就走,除非你的信箱滿了,他必須等信箱空下來(lái).
    有緩沖保證信能到你家信箱
  • 多個(gè)channel選擇數(shù)據(jù)讀取或者寫(xiě)入(select)
使用select關(guān)鍵字,完成“多路選擇”與“超時(shí)控制”。

    select {
    case v := <-ch1:
        fmt.Println("channel 1 msg =>", v)
    case v := <-ch2:
        fmt.Println("channel 2 msg =>", v)
    case <-time.After(time.Millisecond * 100): // 超時(shí)等待
        fmt.Println("time out")
    //default:
    //  fmt.Println("nothing")
    }
使用場(chǎng)景:可以監(jiān)聽(tīng)寫(xiě)信號(hào),進(jìn)程的熱啟動(dòng),配置的熱加載等 

協(xié)程的使用

func TestGroutine(t *testing.T) {
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println(i) // 正確案例,值傳遞。各個(gè)協(xié)程無(wú)競(jìng)爭(zhēng)關(guān)系。
        }(i)

        // go func() {
        //  fmt.Println(i) // 錯(cuò)誤案例,共享變量。各個(gè)協(xié)程有競(jìng)爭(zhēng)關(guān)系, 不安全
        // }()
    }
    time.Sleep(time.Millisecond * 50)
}

  • 協(xié)程并發(fā),導(dǎo)致協(xié)程不安全
    // 協(xié)程不安全demo
    func TestThreadUnsafe(t *testing.T) {
        counter := 0
        for i := 0; i < 5000; i++ {
            go func() {
                counter++
            }()
        }
        time.Sleep(1 * time.Second)
        t.Logf("counter = %d", counter)
    }

    // 輸出結(jié)果如下:
    === RUN   TestThreadUnsafe
    channel_test.go:346: counter = 4742 // 計(jì)算錯(cuò)誤,因?yàn)椴l(fā)導(dǎo)致了漏值
    --- PASS: TestThreadUnsafe (1.00s)
  • 如何保證協(xié)程安全?
    • 方式1: 普通加鎖,并延遲等待協(xié)程執(zhí)行完畢(不推薦)
    // 協(xié)程等待demo(停1秒,不推薦)
    func TestThreadSafe(t *testing.T) {
        var mut sync.Mutex // 互斥鎖
        counter := 0
        for i := 0; i < 5000; i++ {
            go func() { // 開(kāi)啟協(xié)程
                defer func() {
                    mut.Unlock() //函數(shù)調(diào)用完成后:解鎖,保證協(xié)程安全
                }()
                mut.Lock() // 函數(shù)將要調(diào)用前:加鎖,保證協(xié)程安全
                counter++
            }()
        }
        time.Sleep(1 * time.Second) // 等待一秒,等協(xié)程全部執(zhí)行完(如果程序復(fù)雜,1s可能不夠用)
        t.Logf("counter = %d", counter)
    }
    // 輸出結(jié)果如下:
    === RUN   TestThreadSafe
    channel_test.go:363: counter = 5000 
    // 結(jié)果正確,但是有一個(gè)問(wèn)題。因?yàn)檫@里有個(gè)1秒的延遲等待,保證協(xié)程運(yùn)行完畢再調(diào)用結(jié)果
    --- PASS: TestThreadSafe (1.00s)
  • 先來(lái)介紹下:同步等待組(WaitGroup)
waitGroup用于同步協(xié)程同步,等待一組協(xié)程執(zhí)行完畢,才會(huì)繼續(xù)向下執(zhí)行.
1. 主協(xié)程調(diào)用Add()設(shè)置等待的協(xié)程數(shù)量.
2. 協(xié)程執(zhí)行完畢,調(diào)用Done()函數(shù)
3. wait()函數(shù)阻塞,直到所有協(xié)程執(zhí)行完畢才會(huì)繼續(xù)向下執(zhí)行.
  • 方法2 : 使用同步等待隊(duì)列(waitGroup)保證順序執(zhí)行
    // 協(xié)程安全Demo
    func TestWaitGroup(t *testing.T) {
        var mut sync.Mutex    // 互斥鎖
        var wg sync.WaitGroup // 等待隊(duì)列
        counter := 0
        for i := 0; i < 5000; i++ {
            wg.Add(1) // 加個(gè)任務(wù)
            go func() {
                defer func() {
                    mut.Unlock() //函數(shù)調(diào)用完成后:解鎖,保證協(xié)程安全
                }()
                mut.Lock() // 函數(shù)將要調(diào)用前:加鎖,保證協(xié)程安全
                counter++
                wg.Done() // 做完任務(wù)
            }()
        }
        wg.Wait() //等待所有任務(wù)執(zhí)行完畢
        t.Logf("counter = %d", counter)
    }
    // 運(yùn)行結(jié)果如下:
    === RUN   TestWaitGroup
        channel_test.go:382: counter = 5000
    --- PASS: TestWaitGroup (0.00s)

channel的關(guān)閉和廣播

  • close 內(nèi)置函數(shù)關(guān)閉一個(gè)channel,該通道必須是雙向的或僅發(fā)送的
ch1 := make(chan int, 1)
ch2 := make(chan<- int, 1)
ch3 := make(<-chan int, 1)
close(ch1)
close(ch2)
close(ch3) // 報(bào)錯(cuò) invalid operation: close(ch3) (cannot close receive-only channel)
  • 向已經(jīng)關(guān)閉的channel發(fā)送數(shù)據(jù)會(huì)panic
  • 關(guān)閉一個(gè)已經(jīng)關(guān)閉的channel會(huì)panic
  • v, ok <- channel。 其中,ok為bool值,若ok == true時(shí),表示channel處于open狀態(tài)。 若ok==false時(shí),表示channel處于close狀態(tài)。

常見(jiàn)的并發(fā)場(chǎng)景

  • 只執(zhí)行一次(單例模式)
    func TestOnceDo(t *testing.T) {
        var once sync.Once
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Add(1)
            // 在多協(xié)程的情況下,保證某段代碼只執(zhí)行一次。
            go func(ii int) {
                once.Do(func() {
                    t.Log(ii)
                })
                wg.Done()
            }(i)
        }
        wg.Wait()
    }
    // 輸出結(jié)果
    === RUN   TestOnceDo
        channel_test.go:404: 0
    --- PASS: TestOnceDo (0.00s)
  • 發(fā)郵件,發(fā)短信
  • 跑腳本
  • 爬數(shù)據(jù)

總結(jié)

  • 關(guān)鍵字
    • go
    • make
    • chan
    • select
    • sync下的包
      • 互斥鎖
      • 讀寫(xiě)鎖
      • waitGroup
      • map等
  • 將復(fù)雜的任務(wù)拆分, 讓goroutine去并發(fā)的執(zhí)行,通過(guò)channel做數(shù)據(jù)通信
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 轉(zhuǎn)載自:超詳細(xì)的講解Go中如何實(shí)現(xiàn)一個(gè)協(xié)程池 并發(fā)(并行),一直以來(lái)都是一個(gè)編程語(yǔ)言里的核心主題之一,也是被開(kāi)發(fā)者...
    紫云02閱讀 1,071評(píng)論 0 1
  • golang go和php的區(qū)別類型:go為編譯性語(yǔ)言;php解釋性語(yǔ)言錯(cuò)誤:go的錯(cuò)誤處理機(jī)制;php本身或者框...
    Impossible安徒生閱讀 416評(píng)論 0 0
  • go并發(fā)編程入門到放棄 并發(fā)和并行 并發(fā):一個(gè)處理器同時(shí)處理多個(gè)任務(wù)。 并行:多個(gè)處理器或者是多核的處理器同時(shí)處理...
    yangyunfeng閱讀 589評(píng)論 0 2
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂(lè)有人憂愁,有人驚喜有人失落,有的覺(jué)得收獲滿滿有...
    陌忘宇閱讀 8,592評(píng)論 28 53
  • 人工智能是什么?什么是人工智能?人工智能是未來(lái)發(fā)展的必然趨勢(shì)嗎?以后人工智能技術(shù)真的能達(dá)到電影里機(jī)器人的智能水平嗎...
    ZLLZ閱讀 3,850評(píng)論 0 5