您好,登錄后才能下訂單哦!
學習之前先看一下下面這句話:
Don’t communicate by sharing memory; share memory by communicating.
不要通過共享數據來通訊,要以通訊的方式共享數據。
通道(也就是 channel)類型的值可以被用來以通訊的方式共享數據。更具體地說,它一般被用來在不同的goroutine之間傳遞數據。
這篇主要講goroutine是什么。簡單來說,goroutine代表著并發編程模型中的用戶級線程。
Go語言不但有著獨特的并發編程模型,以及用戶級線程goroutine,還擁有強大的用于調度goroutine、對接系統級線程的調度器。
這個調度器是Go語言運行時系統的重要組成部分,它主要負責統籌調配Go并發編程模型中的三個主要元素:
這里需要知道一個與主goroutine有關的重要特性,一旦主goroutine中的代碼(也就是main函數中的那些代碼)執行完畢,當前的 Go 程序就會結束運行。
先看下面這個例子:
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
}
上面的程序運行之后,不會有打印任何內容。
只要go語句本身執行完畢,Go程序完全不會等待go函數的執行,它會立刻去執行后面的語句。這就是所謂的異步并發地執行。
在上面的例子中,在for語句執行完畢后,里面包裝的10個goroutine還沒有獲得運行的機會,主goroutine中的代碼執行完了,Go程序就會立即結束運行。
上面的例子中,如果要讓程序在其他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的時間需要預估。太長會浪費時間,太短則不能保證所有goroutine都運行完畢。不容易預估時間,最好是讓其他的goroutine在運行完畢后發送通知。
使用通道,通道的長度與啟用的goroutine的數量一致。每個goroutine運行完畢前,都向通道發送一個值。在主goroutine則是從這個通道接收值,接收了足夠數量的次數后就說明所有goroutine都運行完畢了,可以繼續往下執行了(就是退出):
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{} ,是一個空結構體。它譚勇的內存空間是0字節。這個值在整個Go程序中永遠都只會存在一份。雖然可以無數次的使用這個值字面量,但是用到的都是同一個值。當把通道僅僅刀座是傳遞某個簡單信號的介質的時候,使用空結構體是最好的。
其他方式
在標準庫中,有一個sync包,里面有一個sync.WaitGroup類型。這應該是一個更好的實現方式。不過這要等后面講sync包的時候再說了。
首先改造一下一只使用的例子,把變量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函數。在trigger里會檢查i,并把要執行的語句打包成fn函數也傳入,只有在trigger里判斷后符合條件,就會執行fn函數的語句。
trigger里會檢查i和count是否相等,在執行了fn函數后,需要把count加1,這里用了原子操作。里有是trigger函數會被多個goroutine并發的調用,所以這個變量被多個用戶級線程共用了。因此對它的操作就產生了競態條件(race condition),破壞了程序的并發安全性。
在最后退出的時候,應該有了trigger函數,只要檢查count是否到10了,就表示其他goroutine都執行完了,所以也就不需要通道了。
另外在trigger函數里,是一個for語句的無限循環,在判斷條件不成立后,先進行了一個1納秒的Sleep。如果不加這句的話,測試下來,偶爾會出現程序卡住的情況(甚至是死機)。這里加上Sleep語句應該是希望這個時候程序可以進行一下切換,否則當前應該執行的那個goroutine如果拿不到執行的機會,其他goroutine也都無法通過if條件的判斷。
自己的實現
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
}
}
主要兩個問題,當時沒有意識到在for無限循環之后,進入下一個迭代前,這個1納秒Sleep的意義。還有就是我沒有使用原子操作。不過這里即使不用原子操作也沒問題的樣子,因為邏輯上通知只有一個goroutine滿足條件會去操作共用的變量count。所以這里和上面講師的示例就差在對變量count的比較和判斷是否是原子操作的問題上了。
原子操作
這里再自我做一些補充。
原子操作,即執行過程不能被中斷的操作(并發)。
經典問題:i++是不是原子操作?
答案是否,因為i++看上去只有一行,但是背后包括了多個操作:取值,加法,賦值。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。