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