這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
(一)並發基礎
1.概念
並發意味著程式在運行時有多個執行內容,對應多個調用棧。
並發與並行的區別:
並發的主流實現模型:
實現模型 |
說明 |
特點 |
多進程 |
作業系統層面的併發模式 |
處理簡單,互不影響,但開銷大 |
多線程 |
系統層面的併發模式 |
有效,開銷較大,高並發時影響效率 |
基於回調的非阻塞/非同步IO |
多用於高並發伺服器開發中 |
編程複雜,開銷小 |
協程 |
使用者態線程,不需要作業系統搶佔調度,寄存於線程中 |
編程簡單,結構簡單,開銷極小,但需要語言的支援 |
共用記憶體系統:線程之間採用共用記憶體的方式通訊,通過加鎖來避免死結或資源競爭。
訊息傳遞系統:將線程間共用狀態封裝在訊息中,通過發送訊息來共用記憶體,而非通過共用記憶體來通訊。
2.協程
執行體是個抽象的概念,在作業系統中分為三個層級:進程(process),進程內的線程(thread),進程內的協程(coroutine,輕量級線程)。協程的數量級可達到上百萬個,進程和線程的數量級最多不超過一萬個。Go語言中的協程叫goroutine,Go標準庫提供的叫用作業,IO操作都會出讓CPU給其他goroutine,讓協程間的切換管理不依賴系統的線程和進程,不依賴CPU的核心數量。
3.並發通訊
並發編程的難度在於協調,協調需要通過通訊,並發通訊模型分為共用資料和訊息。共用資料即多個並發單元保持對同一個資料的引用,資料可以是記憶體資料區塊,磁碟檔案,網路資料等。資料共用通過加鎖的方式來避免死結和資源競爭。Go語言則採取訊息機制來通訊,每個並發單元是獨立的個體,有獨立的變數,不同並發單元間這些變數不共用,每個並發單元的輸入輸出只通過訊息的方式。
(二)goroutine
//定義調用體 func Add(x,y int ){ z:=x+y fmt.Println(z) } //go關鍵字執行調用,即會產生一個goroutine並發執行 //當函數返回時,goroutine自動結束,如果有傳回值,傳回值會自動被丟棄 go Add(1,1) //並發執行 func main(){ for i:=0;i<10;i++{ //主函數啟動了10個goroutine,然後返回,程式退出,並不會等待其他goroutine結束 go Add(i,i) //所以需要通過channel通訊來保證其他goroutine可以順利執行 } } |
(三)channel
channel就像管道的形式,是goroutine之間的通訊方式,是進程內的通訊方式,跨進程通訊建議用分布式系統的方法來解決,例如Socket或http等通訊協定。channel是類型相關,即一個channel只能傳遞一種類型的值,在聲明時指定。
1、基本文法
//1、channel聲明,聲明一個管道chanName,該管道可以傳遞的類型是ElementType //管道是一種複合類型,[chan ElementType],表示可以傳遞ElementType類型的管道[類似定語從句的修飾方法] var chanName chan ElementType var ch chan int //聲明一個可以傳遞int類型的管道 var m map[string] chan bool //聲明一個map,值的類型為可以傳遞bool類型的管道 //2、初始化 ch:=make(chan int ) //make一般用來聲明一個複合類型,參數為複合類型的屬性 //3、管道寫入,把值想象成一個球,"<-"的方向,表示球的流向,ch即為管道 //寫入時,當管道已滿(管道有緩衝長度)則會導致程式堵塞,直到有goroutine從中讀取出值 ch <- value //管道讀取,"<-"表示從管道把球倒出來賦值給一個變數 //當管道為空白,讀取資料會導致程式阻塞,直到有goroutine寫入值 value:= <-ch //4、每個case必須是一個IO操作,面向channel的操作,只執行其中的一個case操作,一旦滿足則結束select過程 //面向channel的操作無非三種情況:成功讀出;成功寫入;即沒有讀出也沒有寫入 select{ case <-chan1: //如果chan1讀到資料,則進行該case處理語句 case chan2<-1: //如果成功向chan2寫入資料,則進入該case處理語句 default : //如果上面都沒有成功,則進入default處理流程 } |
2、緩衝和逾時機制
//1、緩衝機制:為管道指定空間長度,達到類似訊息佇列的效果 c:=make(chan int ,1024) //第二個參數為緩衝區大小,與切片的空間大小類似 //通過range關鍵字來實現依次讀取管道的資料,與數組或切片的range使用方法類似 for i :=range c{ fmt.Println( "Received:" ,i) } //2、逾時機制:利用select只要一個case滿足,程式就繼續執行而不考慮其他case的情況的特性實現逾時機制 timeout:=make(chan bool ,1) //設定一個逾時管道 go func(){ time .Sleep(1e9) //設定逾時時間,等待一秒鐘 timeout<- true //一分鐘後往管道放一個true的值 }() // select { case <-ch: //如果讀到資料,則會結束select過程 //從ch中讀取資料 case <-timeout: //如果前面的case沒有調用到,必定會讀到true值,結束select,避免永久等待 //一直沒有從ch中讀取到資料,但從timeout中讀取到了資料 } |
3、channel的傳遞
//1、channel的傳遞,來實現Linux系統中管道的功能,以外掛程式的方式增加資料處理的流程 type PipeData struct { value int handler func( int ) int //handler是屬性? next chan int //可以把[chan int]看成一個整體,表示放int類型的管道 } func handler(queue chan *PipeData){ //queue是一個存放*PipeDate類型的管道,可改變管道裡的資料區塊內容 for data:=range queue{ //data的類型就是管道存放定義的類型,即PipeData data.next <- data.handler(data.value) //該方法實現將PipeData的value值存放到next的管道中 } } //2、單向channel:只能用於接收或發送資料,是對channel的一種使用限制 //單向channel的聲明 var ch1 chan int //正常channel,可讀寫 var ch2 chan<- int //單向唯寫channel [chan<- int]看成一個整體,表示流入管道 var ch3 <-chan int //單向唯讀channel [<-chan int]看成一個整體,表示流出管道 //管道類型強制轉換 ch4:=make(chan int ) //ch4為雙向管道 ch5:=<-chan int (ch4) //把[<-chan int]看成單向唯讀管道類型,對ch4進行強制類型轉換 ch6:=chan<- int (ch4) //把[chan<- int]看成單向唯寫管道類型,對ch4進行強制類型轉換 func Parse(ch <-chan int ){ //最小許可權原則 for value:=range ch{ fmt.Println( "Parsing value" ,value) } } //3、關閉channel,使用內建函數close()函數即可 close(ch) //判斷channel是否關閉 x,ok:=<-ch //ok==false表示channel已經關閉 if !ok { //如果channel關閉,ok==false,!ok==true //執行體 } |
(四)多核並行化與同步
//多核並行化 runtime.GOMAXPROCS(16) //設定環境變數GOMAXPROCS的值來控制使用多少個CPU核心 runtime.NumCPU() //來擷取核心數 //出讓時間片 runtime.Gosched() //在每個goroutine中控制何時出讓時間片給其他goroutine //同步 //同步鎖 sync.Mutex //單讀單寫:佔用Mutex後,其他goroutine只能等到其釋放該Mutex sync.RWMutex //單寫多讀:會阻止寫,不會阻止讀 RLock() //讀鎖 Lock() //寫鎖 RUnlock() //解鎖(讀鎖) Unlock() //解鎖(寫鎖) //全域唯一性操作 //once的Do方法保證全域只調用指定函數(setup)一次,其他goroutine在調用到此函數是會阻塞,直到once調用結束才繼續 once.Do(setup) |