go語言編程 要點總結(四)併發編程

併發基礎

每個進程只有一個執行上下文,一個調用棧一個堆,操作系統在調度進程時,會保存被調度進程的上下文環境,等待該進程獲得時間片後,再恢復進程上下文。

併發價值

併發能更客觀的表現問題模型 (圖形界面與後臺處理)

併發能充分利用CPU核心的優勢,提高程序的執行效率

併發能充分利用CPU與其他硬件設備固有的異步性(中斷觸發)

實現併發的方式

多進程,操作系統層面的併發,開銷最大的方式。好處是簡單,進程間互不影響,壞處是開銷大,所有進程都是由內核管理的

多線程,在大部分操作系統上都屬於系統層面的併發,最有效,開銷比進程小,在高併發模式下,效率會有影響

基於回調的非阻塞/異步IO,高併發情況下多線程會很快耗盡服務器的內存和CPU,通過事件驅動的方式使系統運轉,壞處是編程複雜,把流程做了分割,對問題本身的反應不夠自然

協程,本質上是用戶態線程,不需要操作系統進行搶佔式調度,而在真正實現中寄存於線程中,因此係統開銷極小。提高任務的併發性,避免多線程缺點。優點是編程簡單,結構清晰,缺點是需要語言支持。

內存共享系統,線程之間通過共享內存的方式,加鎖

消息傳遞系統,線程之間通過消息通信,發送消息時對狀態進行復制,並且在消息傳遞的邊界上交出這個狀態的所有權。由於複製,性能並不優越。

協程

協程可以輕鬆創建上百萬而不會導致系統資源衰竭,線程或進程最多不能超過1萬

多數語言只提供輕量級線程創建,銷燬和切換能力,任何同步IO操作都會阻塞併發執行的輕量級線程

goroutine

go語言的goroutine,在任何系統調用時都會出讓CPU給其他goroutine

定義一個函數,通過go關鍵字調用,這次調用就會在一個新的gotoutine中併發執行,當被調用的函數返回時,這個goroutine自動結束,如果這個函數有返回值,這個返回值會被丟棄

併發通信

工程上,有兩種常見的併發通信模型:共享數據和消息

設計上遵循的通信原則:不要通過共享來通信,而是通過通信來共享

go語言可以支持共享內存和鎖,但是他提供消息機制

消息機制認爲每個併發單元是字包含的獨立的個體,並且都有自己的變量,不同併發單元不共享變量,每個併發單元的輸入和輸出只有一種就是消息

channel

channel是goroutine間的通信方式

channel是進城內通信方式,通過channel傳遞對象過程和調用函數時傳遞參數行爲比較一致

channel是類型相關,一個channel職能傳遞一種類型,需要在聲明channel時指定

通過ch<-把值寫入channel,這個寫入操作在<-ch在讀取之前是阻塞的,這樣就可以確保所有channel執行完畢之後才返回。

var chanName chan ElementType ElementType是指channel傳遞元素的類型

var ch chan int int類型的channel

var m map[string] chan bool 聲明一個map,元素是bool型的channel

ch := make(chan int) 聲明並初始化一個int型的channel

ch <- value 寫入,向channel寫入數據會導致程序阻塞,直到有其他goroutine從這個channel讀取數據

value := <- ch 從channel讀取數據,如果之前沒有寫入數據也會阻塞

select

select用法和switch相似,每個選擇條件用case語句描述,case的每一條語句裏必須是一個IO操作

如果多個case都滿足條件,選取哪個先執行是隨機的

dafault,當監聽的channel都沒有準備好時,默認執行的,select不再阻塞等待channel

緩衝機制

可以指定channel的緩衝區大小,通過make(chan int, 1024)第二個參數

緩衝寫滿之前,不會阻塞

讀取和常規非緩衝channel相同,但可以使用for和range來讀取

for i := range c {

........

}

超時機制

不能永久等待,否則可能出現死鎖

go語言沒有直接提供超時處理機制,但可以利用select機制

timeout := make(chan bool, 1)

go func() {

time.Sleep(1e9)

timeout <- true

}()

select {

case <- ch:

case <- timeout:

// 一直沒有從ch中讀取到數據,但從timeout中讀取到數據

}

這是在go語言中避免channel通信超時最有效辦法

channel的傳遞

go語言中channel是原生類型,自身也可以通過channel傳遞

利用這個特定來實現管道:

type PipeData struct {

value int

handler func(int) int

next chan int

}

func handle(queue chan *PipeData) {

for data := range queue {

data.next <- data.handler(data.value)

}

}

單向channel

將一個chanel變量來那個傳遞給一個函數時,可以通過將其指定爲單向channel,從而限制該函數中對此channel的操作

var ch1 chan int 常規channel

var ch2 char<- float64 單向寫channel

var ch3 <-cha int 單向讀channel

channel初始化和類型轉換

ch4 := makee(chan int)

ch5 := <-chan int(ch4)

ch6 := chan<- int(ch4)

單向channel的意義和c中的const類似,最小權限原則,避免沒必要的使用氾濫問題

關閉channel

close(ch)

通過x, ok := <- ch來判斷一個ch是否已經關閉

應該在生產者的地方關閉channel,而不是消費者,否則容易引起panic

多核並行化

當前版本的go編譯器還不能智能的發現和利用多核的優勢,有可能gotoutine都運行在一個cpu上,一個goroutine執行時,其他的goroutine等待

go語言升級之前,可以設置環境變量GOMAXPROCS來控制使用多少個CPU

或者在代碼中啓動goroutine之前通過runtime.GOMAXPROCS(CPUNUM)來設置

runtime包還提供了NumCPU來獲取核心數

出讓時間片

每個goroutine可以主動出讓時間片,通過runtime包的函數Gosched()

退出當前goroutine

Goexit,退出當前goroutine,但是defer還會繼續調用

同步

即使成功使用了channel有時也難以避免在goroutine之間共享數據

同步鎖

sync.Mutex和sync.RWMutex

Lock()和RLock() 對應的Unlock()和RUnlock()

例子

var l sync.Mutex

func foo() {

l.Lock()

defer l.Unlock()

}

全局唯一性操作

Once類型來保證全局唯一性操作

func setup() {

}

var once sync.Once

once.Do(setup)

Do方法可以保證在全局範圍內只調用指定函數一次,而且其他goroutine在調用到此語句時,將會先備阻塞,直至全局唯一的調用結束

sync包包含一個atomic子包,提供一些基礎數據類型的原子操作函數

func CompareAndSwapUint64(val *uint64, old, new uint64)(swapped bool)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章