年後回來第一篇!老規矩,先上Github
學習Go語言也很有一段時間了。這個東西從年前就開始構思,這兩天終於研究著搞出來了。算是對於goroutine相關的一個練習吧。
###框架概述
框架的入口為MapReduce容器 MRContainer。使用流程如下:
- 初始化一個MRContainer,指定map與reduce執行緒數引數。
- 指定map與reduce執行方法。
- 輸入資料。
- 呼叫Start函式啟動服務。
- 通過GetResult函式獲取結果集。
框架會根據配置開啟指定數目的map與reduce協程,然後輪詢這些協程以分配任務。最終將結果放在一個list儲存的結果集中。
###實現細節
####容器內容 既然要做MapReduce,首先要分析程式中包含哪些東西。
首先map與reduce方法自然是必不可少的。由於協程有框架來進行排程,兩個方法宣告如下:
Mapper func(data interface{})MapperedDataEntry
Reducer func(in MapperedDataSet) interface{}
複製程式碼
如此宣告在實現上限定了mapreduce的操作方式。若是一個map方法需要輸出多個對映結果,可以選擇將輸出channel傳入Mapper方法以供使用。
容器中包括輸入,中轉和輸出三個資料快取區。用來儲存輸入,map結果與reduce結果。
用於goroutine之間通訊的channel也需要儲存起來,由於一個goroutine有多個channel需要管理,宣告瞭一個結構體用來持有這些channel。儲存於一個list用於輪詢。
####工作流程
map與reduce的協程啟動起來基本都是一樣的。所以就直接放在一起說了。
根據go的推薦用法,在goroutine之間使用channel進行通訊。又由於channel操作是阻塞的,若是某個任務執行時間很長,會影響輪詢,或者說輪詢也就基本失去了意義。(必須等待任務執行完成才能繼續)
為了解決這個問題,每個工作協程附帶著啟動了一個心跳協程,在輪詢時通過select同時監聽輸出和心跳協程。心跳協程會每隔一段時間返回當前工作協程的狀態。若收到輸出或者工作協程狀態為空閒,則會給工作協程一個新的任務。否則跳過繼續輪詢。
啟動時,首先啟動指定數目的工作協程,並將持有的channel儲存到容器中,下面以map協程為例:
//map協程方法
func (container *MRContainer) mapWrapper (in <-chan interface{}, out chan<- MapperedDataEntry, system chan int,cb chan int){
mapper := container.Mapper
workState := SIGN_BEEP_FREE // 當前工作狀態,若為0,則可以輸入,否則正忙
beepChan := make(chan int)
go beep(&workState,cb,beepChan) // 啟動心跳
for {
shutdownFlag := false
select {
case src,ok := <-in:
if !ok {
fmt.Print("not ok !")
}
workState = SIGN_BEEP_WORKING
entry := mapper(src)
workState = SIGN_BEEP_FREE
out<-entry
case command,ok := <-system:
if !ok {
fmt.Print("not ok !")
}
fmt.Println("recive shutdown sign :", strconv.Itoa(command))
if(command == SIGN_CTRL_SHUTDOWN){
shutdownFlag = true
}
}
if(shutdownFlag){
break;
}
}
fmt.Println("goroutines has been finish")
}
複製程式碼
此時工作協程處於空閒狀態,等待接收資料。然後就要在容器協程啟動輪詢開始分發資料,同樣以map操作為例:
func (container *MRContainer) startMap (){
fmt.Println("container doing map work")
holder := container.holdMapChans.Front();
inbuffer := container.inBuffer
for {
fail2Stop := false // 判斷是否可以停止map工作,當快取清空且所有工作均已經完成即可
if chans,ok := holder.Value.(mapChanSet);ok{
select {
case entry,ok :=<-chans.out:
if !ok {
fmt.Print("not ok !")
}
container.midBuffer.put(entry.key,entry.value)
e := inbuffer.pull()
if e!=nil {
chans.in <- e
fail2Stop = true //還有人沒完成工作呢,哼哼
}
case state,ok := <-chans.beep:
if !ok {
fmt.Print("not ok !")
}
if state==0 {
e := inbuffer.pull()
if e!=nil {
chans.in <- e
fail2Stop = true //還有人沒完成工作呢,哼哼
}else{
chans.system <- SIGN_CTRL_SHUTDOWN // 當前已經沒有更多資料,且此協程已經完成工作,發出關閉訊號
}
}else{
fail2Stop = true // 嘗試停止服務失敗,還有協程沒有完成任務
}
}
}
if(!fail2Stop){
break;
}
//維持輪詢
ele := holder.Next()
if ele==nil{
holder = container.holdMapChans.Front()
}
}
}
複製程式碼
###問題總結
在開發過程當中當然會遇到很多問題,從中也吸取了很多教訓。
#####Go中的型別校驗:
與java不同,在Java中,不明真相的型別之間可以互相進行轉換,轉換失敗才會報錯。在Go中未經過型別校驗直接轉換是會報錯的。應當也可以算是某種強制的程式設計規範吧。做法如下:
if ele,ok:=e.Value.(MapperedDataSet);ok{
chans.in <- ele
fail2Stop = true //還有人沒完成工作呢,哼哼
}
複製程式碼
#####channel的阻塞
在編碼中最初遇到的問題之一就是由於channel阻塞導致各種死鎖崩潰。go能夠自動判斷死鎖並結束程式還是非常好的。但是出問題就很糾結了。
很多種情況會導致崩潰,不過我遇到的問題基本就是由於一個channel進行了多次讀取。在多執行緒的程式中這種問題真的很隱蔽,找了很久才找明白原因。換用單項channel可以很有效的避免這種問題。
#####指標和值的使用
一開始的時候在容器中直接使用變數來儲存資料,後來發現有些資料的修改一直都不管用,直到最後換用指標方式才成功執行。
這是一個很大的話題,還需要仔細研究,估計之後還可以再單發一篇簡書出來。
###總結
不得不說用Go來編寫多執行緒的程式真的是太方便了,比起java來說不知道高明到哪裡去了。執行緒之間的通訊等也十分簡潔。不需要過多囉嗦的程式碼就可以很流暢的實現通訊。
通過這個框架的實現,算是比較全面的練習了一下go語言的核心功能:goroutine,以及相關的技術。同時也算裝了一下MapReduce的B。原本神祕的技術其實最簡單的實現起來原理並不困難。
新的一年,從此開始!