這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
goroutine之間的同步
goroutine是golang中在語言層級實現的輕量級線程,僅僅利用go就能立刻起一個新線程。多線程會引入線程之間的同步問題,經典的同步問題如生產者-消費者問題,在c,java層級需要使用鎖、訊號量進行共用資源的互斥使用和多線程之間的時序控制,而在golang中有channel作為同步的工具。
1. channel實現兩個goroutine之間的通訊
package mainimport "strconv"import "fmt"func main() {taskChan := make(chan string, 3)doneChan := make(chan int, 1)for i := 0; i < 3; i++ {taskChan <- strconv.Itoa(i)fmt.Println("send: ", i)}go func() {for i := 0; i < 3; i++ {task := <-taskChanfmt.Println("received: ", task)}doneChan <- 1}()<-doneChan}
- 建立一個channel,
make(chan TYPE {, NUM})
, TYPE指的是channel中傳輸的資料類型,第二個參數是可選的,指的是channel的容量大小。
- 向channel傳入資料,
CHAN <- DATA
, CHAN指的是目的channel即收集資料的一方,DATA則是要傳的資料。
- 啟動一個goroutine接收main routine向channel發送的資料,
go func(){ BODY }()
建立一個線程運行一個匿名函數。
- 從channel讀取資料,
DATA := <-CHAN
,和向channel傳入資料相反,在資料輸送箭頭的右側的是channel,形象地展現了資料從‘隧道’流出到變數裡。
- 通知主線程任務執行結束,doneChan的作用是為了讓main routine等待這個剛起的goroutine結束,這裡顯示了channel的另一個特性,如果從empty channel中讀取資料,則會阻塞當前routine,直到有資料可以讀取。
上面這個程式就是main routine向另一個routine發送了3條int類型的資料,當3條資料被接收到後,主線程也從阻塞狀態恢複運行,隨後結束。
2. 不要陷入“死結”
我一開始用channel的時候有報過"fatal error: all goroutines are asleep - deadlock! "的錯誤,真實的代碼是下面這樣的:
package mainimport "fmt"func main() {ch := make(chan int)ch <- 1 // I'm blocked because there is no channel read yet. fmt.Println("send")go func() {<-ch // I will never be called for the main routine is blocked!fmt.Println("received")}()fmt.Println("over")}
我的本意是從main routine發送給另一個routine一個int型的資料,但是運行出了上述的錯誤,原因有2個:
- 當前routine向channel發送/接收資料時,如果另一端沒有相應地接收/發送,那麼當前routine則會進行休眠。
- 這個程式的main routine先行在
ch <- 1
進入休眠狀態,程式的餘下部分根本來不及運行,那麼channel裡的資料永遠不會被讀出,也就不能喚醒main routine,進入“死結”。
解決這個“死結”的方法可是是設定channel的容量大小大於1,那麼channel就不會因為資料輸入而阻塞主程; 或者將資料輸入channel的語句置於啟動新的goroutine之後。
3. channel作為狀態轉移的訊號源
我跟著MIT的分散式運算課程做了原型為Map-Reduce的課後練習,目的是實現一個Master向Worker指派任務的功能:Master伺服器去等待Worker伺服器串連註冊,Master先將Map任務和Reduce任務指派給這些註冊Worker,等待Map任務全部完成,然後將Reduce任務再分配出去,等待全部完成。
// Initialize a channel which records the process of the map jobs.mapJobChannel := make(chan string)// Start a goroutine to send the nMap(the number of the Map jobs) tasks to the main routine.go func() {for m := 0; m < nMap; m++ {// Send the "start a Map job <m>" to the receiver.mapJobChannel <- "start, " + strconv.Itoa(m)}}()// Main routine listens on this mapJobChannel for the Map job task information.nMapJobs := nMap// Process the nMap Map tasks util they're all done.for nMapJobs > 0 {// Receive the Map tasks from the channel.taskInfo := strings.Split(<-mapJobChannel, ",")state, mapNo := taskInfo[0], taskInfo[1]if state == "start" {// Assign a Map task with number mapNo to a worker.go func() {// Wait for a worker to finish the Map task.ok, workNo := assignJobToWorker("Map", mapNo)if ok {// Send a task finish signal and set the worker's state to idle.mapJobChannel <- "end, " + mapNosetWorkerState(workNo, "idle")} else {// Restart this task and set the worker's state to finished.mapJobChannel <- "start, " + mapNosetWorkerState(workNo, "finished")}}()} else {nMapJobs--}}
以上是我截取自己寫的代碼,關於用channel來傳遞當前Map任務的進度資訊,用類似訊號的方式標註當前的任務執行狀態。
- 當從channel中讀取到"start, {NUM}"時找一個閒置Worker去執行Map任務,並且等待它的完成,完成成功則向channel中發送"end, {NUM}"訊號,表明任務完成,如果失敗,就重發"start, {NUM}"訊號。
- 從channel中讀取到"end, {NUM}"時,把剩餘任務數減1。
這種訊號觸發的方式,觸發Master的狀態轉移,並且可以通過增加訊號以及訊號處理的方式,拓展業務處理的情況,暫時還能處理這個需求情景。
MIT分布式系統課程