前言:由於 LeetCode Concurrency(併發) 還沒有 Go 語言版本,我先自行用 Go 語言來解題。為了能在 LeetCode 以外的平台獲得討論,所以我打算逐漸把自己的解題思路寫下。這是我試水溫的第一篇。
本題 LeetCode 連結:
https://leetcode.com/problems/print-foobar-alternately/
本題考核點?
指定次數交替執行 printFoo()
與 printBar()
。若任由兩個 goroutine 分別各自 print,不能保證其結果一定會互相交錯。
goroutine 若不刻意控制,將無法保證執行的先後順序,因此本題就是要考核對 goroutine 來回交錯順序控制的能力。
解法與思路:
1. 所用 Channel 型態與定位?
本題採用三個 unbuffered channel
1 | type FooBar struct { |
分別是:
streamFooToBar
:Foo()
交棒給Bar()
streamBarToFoo
:Bar()
交棒給Foo()
streamEnd
: 結束訊號
2. Foo()
與 Bar()
如何交接棒?
1 | func (this *FooBar) Foo(printFoo func()) { |
- 根據設定次數 n 重複執行
- 每一輪都要得到
Bar()
交出棒,才會執行printFoo()
以印出字串 - 印出字串後,以
this.streamFooToBar <- struct{}{}
交棒給Bar()
下方的 Bar()
也是一樣的道理。
1 | func (this *FooBar) Bar(printBar func()) { |
3. Foo()
與 Bar()
如何收尾?
這裡要特別注意的是,Foo()
與 Bar()
只有差異在最後一行,用意是什麼?
多個 goroutine 用 unbuffered channel 互相交接棒,會有一個尷尬的情況,就是互為消費者、又互為生產者,因此先 return 的 goroutine 沒事,但是後 return 的 goroutine 會由於消費者消失,send to channel 的時候發生 Deadlock。
根據本題遊戲規則, printBar()
一定要比 printFoo()
晚執行,因此不做特別處理的話,會在 Bar()
試圖做最後一次交棒時,由於消費者消失發生 Deadlock。
我的應對方式,就是讓 Foo()
return 前做一次無特別作用的接棒,這樣就可以避免 Bar()
return 前找不到消費者的問題。
4. 自循環啟動
1 | fooBar.streamBarToFoo <- struct{}{} //啟動 |
前面說過,本題解法採用 Foo()
與 Bar()
彼此循環交棒的方式,因此不能自行啟動,需要外界給訊號,所以在 main()
執行各個 goroutine
以後以 fooBar.streamBarToFoo <- struct{}{}
,讓 main()
假裝自己是 Bar()
交棒給 Foo()
,以啟動交接棒循環。
5. 特別條件下,用 unbuffered channel 取代 sync.WaitGroup
1 | <-fooBar.streamEnd //as wg.Wait() |
為了等待多個 goroutine 都結束再往下,一般來說用 sync.WaitGroup.Wait()
是標準做法。但這一題還有更輕量的方法。
雖然這一題是 Concurrency,但是各個 goroutine 的結束順序已經被定死,我們很清楚知道誰負責收尾,所以讓負責收尾的 goroutine send to unbuffered channel,然後在 main()
read 掉,這樣就不需要使用 sync.WaitGroup
執行各個 goroutine 以後,main()
會由於 <-fooBar.streamEnd
還沒有被傳入而被卡住,這就相當於 sync.WaitGroup.Wait()
的作用了。
由於是 Bar()
會做最後一次有意義的執行,因此讓 Bar()
return 之前執行 this.streamEnd <- struct{}{}
,這就相當於交棒給 main()
, main()
終於可以從被卡住的 <-fooBar.streamEnd
往下(因為終於有東西可以讀),就像便秘了三天突然暢通一樣!
完整解題程式碼:
https://play.golang.org/p/YsXKHbxpOCT