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