go中如何使用easyjson_如何在 Go 中编写无 Bug 的 Goroutines?
點(diǎn)上方藍(lán)色“云原生領(lǐng)域”關(guān)注我,設(shè)個(gè)星標(biāo),不會(huì)讓你失望
GO 并發(fā)
Go 以其并發(fā)性著稱,深受人們喜愛。go 運(yùn)行時(shí)管理輕量級(jí)線程,稱為 goroutines。goroutine 的編寫非??焖俸?jiǎn)單。
你只需在你想異步執(zhí)行的函數(shù)前輸入 go,程序就會(huì)在另一個(gè)線程中執(zhí)行。
聽起來很簡(jiǎn)單?
goroutines 是 Go 編寫異步代碼的方式。
重要的是要了解 goroutine 和并發(fā)的工作原理。Go 提供了管理 goroutine 的方法,使它們?cè)趶?fù)雜的程序中更容易管理和預(yù)測(cè)。
“因?yàn)?goroutine 非常容易使用,所以它們很容易被濫用。
”1 在異步例程中不要對(duì)執(zhí)行順序進(jìn)行假設(shè)。
在 Go 中調(diào)度并發(fā)任務(wù)時(shí),要記住異步任務(wù)的不可預(yù)知性。
可以將異步與同步計(jì)算融合在一起,但只要同步任務(wù)不對(duì)異步任務(wù)做任何假設(shè)即可。
對(duì)于初學(xué)者來說,一個(gè)常見的錯(cuò)誤是創(chuàng)建一個(gè) goroutine,然后根據(jù)該 goroutine 的結(jié)果繼續(xù)執(zhí)行同步任務(wù)。例如,如果該 goroutine 要向其作用域外的變量寫入,然后在同步任務(wù)中使用該變量。
假設(shè)執(zhí)行順序
package?mainimport?(
?"time"
?"fmt"
)
func?main()?{
?var?numbers?[]int?//?nil
?//?start?a?goroutine?to?initialise?array
?go?func?()?{
??numbers?=?make([]int,?2)
?}()
??
??//?do?something?synchronous
??if?numbers?==?nil?{
????time.Sleep(time.Second)
??}
??numbers[0]?=?1?//?will?sometimes?panic?here
?fmt.Println(numbers[0])
}
這種模式會(huì)導(dǎo)致不可預(yù)知的行為。它引入的代碼導(dǎo)致了我們無法控制的因素;這些因素與 go 運(yùn)行時(shí)有關(guān),更具體地說,就是它如何管理 goroutines。
“編寫這樣的代碼意味著假定 goroutine 將在需要結(jié)果之前完成它的任務(wù)。
”首先,在沒有某種管理技術(shù)(我們將討論)的情況下,交叉異步和同步代碼的成功將取決于 CPU 的可用性。
這意味著如果有 CPU 密集型的進(jìn)程與 goroutines 同時(shí)運(yùn)行,那么執(zhí)行的時(shí)間將會(huì)有所不同。
其次,不同的編譯器將以不同的方式調(diào)度 goroutines。因此,安全的做法是不要認(rèn)為 goroutine 會(huì)在同步任務(wù)期間完成。
如何確保 goroutine 已經(jīng)完成?
“使用 channel
”在異步任務(wù)完成時(shí)使用 channel 來通知
channel 應(yīng)該用于接收來自異步任務(wù)(如 goroutines)的值。
如果你想阻止進(jìn)一步的執(zhí)行,直到最終從 channel 讀取一個(gè)值來釋放它,可以使用緩沖通道。
如果你想要 1 進(jìn) 1 出的行為,那么使用非緩沖通道。
在本例中,使用 channel,我們可以確保主任務(wù)等待直到異步任務(wù)完成。當(dāng) goroutine 完成它的工作時(shí),它將通過 done channel 發(fā)送一個(gè)值,該值將在對(duì) numbers 數(shù)組進(jìn)行操作之前被讀取。
package?mainimport?(
?"time"
?"fmt"
)
func?main()?{
?var?numbers?[]int?//?nil
?done?:=?make(chan?struct{})
?//?start?a?goroutine?to?initialise?array
?go?func?()?{
??numbers?=?make([]int,?2)
??done?struct{}{}
?}()
??
??//?do?something?synchronous
??//?read?done?from?channel
??numbers[0]?=?1?//?will?not?panic?anymore
?fmt.Println(numbers[0])?//?1
}
盡管這是一個(gè)人為的示例,但你可以看到它在什么地方會(huì)很有用:當(dāng)主線程與 goroutine 并行處理復(fù)雜工作時(shí)。這兩個(gè)任務(wù)可以同時(shí)完成,而不可能出現(xiàn) panic。
2 避免跨并發(fā)線程訪問可變數(shù)據(jù)
跨多個(gè) goroutine 訪問可變數(shù)據(jù)是將數(shù)據(jù)競(jìng)爭(zhēng)引入程序的“好方法”。
數(shù)據(jù)競(jìng)爭(zhēng)是指兩個(gè)或多個(gè)線程(或這里的goroutine)并發(fā)訪問同一內(nèi)存位置。
這意味著跨線程訪問相同的變量可能會(huì)產(chǎn)生不可預(yù)測(cè)的值。如果兩個(gè)進(jìn)程同時(shí)訪問同一個(gè)變量,有兩種可能性:
- 兩個(gè)線程的值是相同的(不正確)。
- 對(duì)于較慢/較晚的線程,該值是不同的。(正確)
如果較慢/較晚的線程讀取了一個(gè)已被較快/較早的線程修改過的更新值,那么它將對(duì)更新后的值進(jìn)行操作。這是預(yù)期的行為。
否則,就像在數(shù)據(jù)競(jìng)爭(zhēng)中看到的那樣,兩個(gè)線程將產(chǎn)生相同的值,因?yàn)樗鼈兌紝?duì)未更改的值進(jìn)行操作。
1000 種可能的數(shù)據(jù)競(jìng)爭(zhēng)
在這個(gè)例子中,我們使用 sync.WaitGroup 來保持程序運(yùn)行,直到所有的 goroutine 完成,但我們并沒有控制對(duì)每個(gè) goroutine 內(nèi)變量的訪問。
package?mainimport?(
?"fmt"
?"sync"
)
func?main()?{
?a?:=?0?//?data?race
?var?wg?sync.WaitGroup
?wg.Add(1000)
?for?i?:=?0;?i?1000;?i++?{
??go?func()?{
???defer?wg.Done()
???a?+=?1
??}()
?}
?wg.Wait()
???fmt.Println(a)?//?could?theoretical?be?any?number?0-1000?(most?likely?above?900)
}
這段代碼可以打印 0-1000 之間的任何數(shù)字,具體取決于發(fā)生的數(shù)據(jù)競(jìng)爭(zhēng)數(shù)量。
這段代碼的工作原理是,兩個(gè)線程將對(duì)同一個(gè)變量各執(zhí)行 2 次操作,總共有 2 次讀 + 2 次寫。
“在兩個(gè)線程都會(huì)產(chǎn)生相同的值的情況下,在對(duì)變量進(jìn)行任何寫入之前,兩個(gè)(2)讀都必須發(fā)生。
”使用互斥鎖在 goroutines 之間共享內(nèi)存
為了防止 goroutines 中的數(shù)據(jù)競(jìng)爭(zhēng),我們需要同步對(duì)共享內(nèi)存的訪問。我們可以使用互斥來實(shí)現(xiàn)這一點(diǎn)?;コ怄i將確保我們不會(huì)在同一時(shí)間讀取或?qū)懭胂嗤闹怠?/p>
它本質(zhì)上是暫時(shí)鎖定對(duì)一個(gè)變量的訪問。
package?mainimport?(
?"fmt"
?"sync"
)
func?main()?{
?a?:=?0
?var?wg?sync.WaitGroup
?var?mu?sync.Mutex?//?guards?access
?wg.Add(1000)
?for?i?:=?0;?i?1000;?i++?{
??go?func()?{
???mu.Lock()
???defer?mu.Unlock()
???defer?wg.Done()
???a?+=?1
??}()
?}
?wg.Wait()
?fmt.Println(a)?//?will?always?be?1000
}
就這么簡(jiǎn)單。
這段代碼總是打印 1000,因?yàn)閷?duì)同一個(gè)變量的每個(gè)后續(xù)操作都會(huì)對(duì)更新后的值進(jìn)行操作。
3 不要寫應(yīng)該同步的異步任務(wù)
Goroutines 通常被認(rèn)為是后臺(tái)任務(wù)。它們被視為可以與主程序同時(shí)運(yùn)行的小任務(wù),通過 goroutine 將其委托給另一個(gè)線程。
當(dāng)學(xué)習(xí) Go 時(shí),你往往會(huì)想到使用 goroutine 來盡量減少阻塞操作,或者讓我們的程序性能更強(qiáng)。
但由于對(duì) goroutine 的看法如此簡(jiǎn)單,很容易養(yǎng)成 "以防萬一" 的習(xí)慣,把所有東西都做成 goroutine。
如果某些任務(wù)本質(zhì)上是同步的,但你卻異步地使用了它們,這就會(huì)造成問題。
并非所有的任務(wù)都應(yīng)該是一個(gè) goroutine。
有些任務(wù)需要秩序。在許多進(jìn)程中,下一個(gè)任務(wù)取決于前一個(gè)任務(wù)的結(jié)果。這些順序性的任務(wù)會(huì)讓你的程序出錯(cuò),勢(shì)必需要讓這些區(qū)域更加同步。
所以有些情況下,你還不如直接忘掉goroutine,一開始就保持同步。
用無限循環(huán)浪費(fèi) CPU
在這個(gè)精心設(shè)計(jì)的示例中,我們有一個(gè)程序,它將所有內(nèi)容委托給 goroutines,并使用 for 循環(huán)來保持程序運(yùn)行。
這是一個(gè)如何不控制 Go 程序流程的例子。
func?main()?{??go?doSomething()
??go?doSomethingElse()
??
??//?execute?everything?as?a?goroutine
??
??for?{?//?this?keeps?the?program?running
??
??}
}
最好保持簡(jiǎn)單。你可以通過把你的程序看作是主線程加上附加線程的方式來防止這種類型的不良做法。你可以讓主線程以同步的方式運(yùn)行,但如果需要,可以通過 goroutines 將任務(wù)委托給另一個(gè)線程。
有更好的方法可以控制程序的流程,比如通過 WaitGroups 或 Channels。
使用 WaitGroup 的控制流程
與其浪費(fèi)寶貴的 CPU 資源,不如使用 WaitGroup 向運(yùn)行時(shí)表明,在程序退出之前,你正在等待 n 個(gè)任務(wù)的完成。這樣就不會(huì)讓 CPU 一直在無限循環(huán)中旋轉(zhuǎn)。
func?doSomething(wg?*sync.WaitGroup)?{??//?do?something?here
??fmt.Println("Done")
??defer?wg.Done()
}
func?main()?{
??var?wg?sync.WaitGroup
??wg.Add(1)
??defer?wg.Wait()
??go?doSomething(&wg)
??go?doSomethingElseSync()
??
??//?program?will?wait?until?doSomething?&?doSomethingElseSync?is?complete
}
首先,您需要將等待完成的任務(wù)數(shù)量作為參數(shù)提供給 wg.Add() 函數(shù)。
放置 wg.Wait() 很重要。這是程序中執(zhí)行將暫停的地方,等待所有任務(wù)完成。
一旦任務(wù)完成,您可以使用 wg.Done() 讓程序知道。
4 不要讓 goroutines 掛起
確保處理不再使用的 goroutines。持續(xù)運(yùn)行的 Goroutines 將會(huì)阻塞并浪費(fèi)寶貴的 CPU 資源。
如果 goroutine 試圖將值發(fā)送到?jīng)]有任何讀取并等待接收值的 channel,就會(huì)發(fā)生這種情況。這就意味著這條 channel 將永遠(yuǎn)卡在那里。
9 個(gè)掛起的 goroutine
在這個(gè)例子中,channel 只被讀取一次。這意味著 9 個(gè) goroutines 在等待通過 channel 發(fā)送一個(gè)值。
func?sendToChan()?int?{?channel?:=?make(chan?int)
?for?i?:=?0;?i?10;?i++?{
??i?:=?i
??go?func()?{
???channel?//?9?hanging?goroutines
??}()
?}
?return?}
為了避免這種情況,請(qǐng)?zhí)幚聿辉傩枰?goroutines 來釋放 CPU。
使通道緩沖
使用緩沖通道意味著您正在為通道提供空間來存儲(chǔ)附加值。
對(duì)于當(dāng)前的示例,這意味著所有的 goroutines 都將成功執(zhí)行,不會(huì)阻塞。
func?sendToChan()?int?{?channel?:=?make(chan?int,?9)
?for?i?:=?0;?i?10;?i++?{
??i?:=?i
??go?func()?{
???channel?//?all?goroutines?executed?successfully
??}()
?}
?return?}
不要在不知道什么時(shí)候停止的情況下開始一個(gè) goroutine。
在不知道何時(shí)停止的情況下啟動(dòng)一個(gè) goroutine 會(huì)導(dǎo)致以下行為,即 goroutine 被阻塞或浪費(fèi) CPU 資源。
您應(yīng)該總是知道什么時(shí)候 goroutine 將停止,什么時(shí)候不再需要它。
您可以通過 select 語句和 channel 來實(shí)現(xiàn)這一點(diǎn)
done?:=?make(chan?bool)go?func()?{
??for?{
????select?{
??????case?????????return
???????default:
????}
??}
}()
done?true
這本質(zhì)上是一個(gè)帶有退出條件的異步 for-loop。
重要的邏輯將在默認(rèn)條件下編寫。
“當(dāng)值被發(fā)送到 done 通道時(shí),循環(huán)將停止,正如 done 所示。這意味著 channel 讀取 成功并返回。
”“譯自:https://itnext.io/how-to-write-bug-free-goroutines-in-go-golang-59042b1b63fb
”來都來了,點(diǎn)個(gè)“再看”再走叭~~總結(jié)
以上是生活随笔為你收集整理的go中如何使用easyjson_如何在 Go 中编写无 Bug 的 Goroutines?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cpuz北桥频率和内存频率_DDR4的内
- 下一篇: c# ef报错_C#中Entity Fr