轉自:https://blog.csdn.net/huwh_/article/details/74858134
(一)併發基礎
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) |