Go併發編程

轉自: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(intint   //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)

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