併發基礎
每個進程只有一個執行上下文,一個調用棧一個堆,操作系統在調度進程時,會保存被調度進程的上下文環境,等待該進程獲得時間片後,再恢復進程上下文。
併發價值
併發能更客觀的表現問題模型 (圖形界面與後臺處理)
併發能充分利用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)