溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go36-16,17-goroutine

發(fā)布時間:2020-07-16 13:10:03 來源:網(wǎng)絡(luò) 閱讀:270 作者:騎士救兵 欄目:編程語言

go語句及其執(zhí)行規(guī)則

學(xué)習(xí)之前先看一下下面這句話:

Don’t communicate by sharing memory; share memory by communicating.
不要通過共享數(shù)據(jù)來通訊,要以通訊的方式共享數(shù)據(jù)。

通道(也就是 channel)類型的值可以被用來以通訊的方式共享數(shù)據(jù)。更具體地說,它一般被用來在不同的goroutine之間傳遞數(shù)據(jù)。
這篇主要講goroutine是什么。簡單來說,goroutine代表著并發(fā)編程模型中的用戶級線程。

調(diào)度器

Go語言不但有著獨特的并發(fā)編程模型,以及用戶級線程goroutine,還擁有強(qiáng)大的用于調(diào)度goroutine、對接系統(tǒng)級線程的調(diào)度器。
這個調(diào)度器是Go語言運行時系統(tǒng)的重要組成部分,它主要負(fù)責(zé)統(tǒng)籌調(diào)配Go并發(fā)編程模型中的三個主要元素:

  • G(goroutine 的縮寫),用戶級線程
  • P(processor 的縮寫),一種可以承載若干個G,且能夠使用這些G適時的與M進(jìn)行對接,并得到真正運行的中介
  • M(machine 的縮寫),系統(tǒng)級線程

主goroutine

這里需要知道一個與主goroutine有關(guān)的重要特性,一旦主goroutine中的代碼(也就是main函數(shù)中的那些代碼)執(zhí)行完畢,當(dāng)前的 Go 程序就會結(jié)束運行。
先看下面這個例子:

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}

上面的程序運行之后,不會有打印任何內(nèi)容。
只要go語句本身執(zhí)行完畢,Go程序完全不會等待go函數(shù)的執(zhí)行,它會立刻去執(zhí)行后面的語句。這就是所謂的異步并發(fā)地執(zhí)行。
在上面的例子中,在for語句執(zhí)行完畢后,里面包裝的10個goroutine還沒有獲得運行的機(jī)會,主goroutine中的代碼執(zhí)行完了,Go程序就會立即結(jié)束運行。

使用Sleep等待

上面的例子中,如果要讓程序在其他goroutine運行完之后再退出。最簡單粗暴的辦法是Sleep一段時間:

package main

import (
    "fmt"
    "time"
)

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
    time.Sleep(time.Second)
}

這個辦法可行,但是Sleep的時間需要預(yù)估。太長會浪費時間,太短則不能保證所有g(shù)oroutine都運行完畢。不容易預(yù)估時間,最好是讓其他的goroutine在運行完畢后發(fā)送通知。

讓主goroutine等待其他goroutine

使用通道,通道的長度與啟用的goroutine的數(shù)量一致。每個goroutine運行完畢前,都向通道發(fā)送一個值。在主goroutine則是從這個通道接收值,接收了足夠數(shù)量的次數(shù)后就說明所有g(shù)oroutine都運行完畢了,可以繼續(xù)往下執(zhí)行了(就是退出):

package main

import "fmt"

func main() {
    sign := make(chan struct{}, 10)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
            sign <- struct{}{}
        }()
    }
    for j := 0; j < 10; j++ {
        <- sign
    }
}

這里聲明的通道的類型是 chan struct{} ,是一個空結(jié)構(gòu)體。它譚勇的內(nèi)存空間是0字節(jié)。這個值在整個Go程序中永遠(yuǎn)都只會存在一份。雖然可以無數(shù)次的使用這個值字面量,但是用到的都是同一個值。當(dāng)把通道僅僅刀座是傳遞某個簡單信號的介質(zhì)的時候,使用空結(jié)構(gòu)體是最好的。
其他方式
在標(biāo)準(zhǔn)庫中,有一個sync包,里面有一個sync.WaitGroup類型。這應(yīng)該是一個更好的實現(xiàn)方式。不過這要等后面講sync包的時候再說了。

讓多個goroutine按照既定的順序執(zhí)行

首先改造一下一只使用的例子,把變量i的值傳遞給每個goroutine,這樣輸出的是0-9各一次,不過是亂序的:

for i := 0; i < 10; i++ {
    go func(i int) {
        fmt.Println(i)
        sign <- struct{}{}
    }

講師的例子

package main

import (
    "fmt"
    "sync/atomic"
    "time"
)

var count uint32

func trigger (i uint32, fn func()) {
    for {
        if n := atomic.LoadUint32(&count); n == i {
            fn()
            atomic.AddUint32(&count, 1)
            break
        }
        time.Sleep(time.Nanosecond)
    }
}

func main() {
    for i := uint32(0); i < 10; i++ {
        go func(i uint32) {
            fn := func() {
                fmt.Println(i)
            }
            trigger(i, fn)
        }(i)
    }
    trigger(10, func() {})
}

主要就是trigger函數(shù)。在trigger里會檢查i,并把要執(zhí)行的語句打包成fn函數(shù)也傳入,只有在trigger里判斷后符合條件,就會執(zhí)行fn函數(shù)的語句。
trigger里會檢查i和count是否相等,在執(zhí)行了fn函數(shù)后,需要把count加1,這里用了原子操作。里有是trigger函數(shù)會被多個goroutine并發(fā)的調(diào)用,所以這個變量被多個用戶級線程共用了。因此對它的操作就產(chǎn)生了競態(tài)條件(race condition),破壞了程序的并發(fā)安全性。
在最后退出的時候,應(yīng)該有了trigger函數(shù),只要檢查count是否到10了,就表示其他goroutine都執(zhí)行完了,所以也就不需要通道了。
另外在trigger函數(shù)里,是一個for語句的無限循環(huán),在判斷條件不成立后,先進(jìn)行了一個1納秒的Sleep。如果不加這句的話,測試下來,偶爾會出現(xiàn)程序卡住的情況(甚至是死機(jī))。這里加上Sleep語句應(yīng)該是希望這個時候程序可以進(jìn)行一下切換,否則當(dāng)前應(yīng)該執(zhí)行的那個goroutine如果拿不到執(zhí)行的機(jī)會,其他goroutine也都無法通過if條件的判斷。

自己的實現(xiàn)

package main

import (
    "fmt"
    "time"
)

func main() {
    sign := make(chan struct{}, 10)
    var count int
    for i := 0; i < 10; i++ {
        go func(i int) {
            for {
                if count == i{
                    fmt.Println(i)
                    count ++
                    sign <- struct{}{}
                    break
                }
                time.Sleep(time.Nanosecond)
            }
        }(i)
    }
    for j := 0; j < 10; j++ {
        <- sign
    }
}

主要兩個問題,當(dāng)時沒有意識到在for無限循環(huán)之后,進(jìn)入下一個迭代前,這個1納秒Sleep的意義。還有就是我沒有使用原子操作。不過這里即使不用原子操作也沒問題的樣子,因為邏輯上通知只有一個goroutine滿足條件會去操作共用的變量count。所以這里和上面講師的示例就差在對變量count的比較和判斷是否是原子操作的問題上了。

原子操作
這里再自我做一些補充。

原子操作,即執(zhí)行過程不能被中斷的操作(并發(fā))。
經(jīng)典問題:i++是不是原子操作?
答案是否,因為i++看上去只有一行,但是背后包括了多個操作:取值,加法,賦值。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI