譯自:Designing for Concurrency: the Hilbert’s Hotel Problem in Go,本文使用go的並行性來解決Hilbert酒店問題。本文比較有意思的是它對問題的描述很吸引人,在看完文字描述之後,程式碼實現邏輯也基本順理成章,當然程式碼本身的實現也相當優雅。
文章一開始敘述了並行和並行的區別和聯絡,此處略去該部分。
Hilbert酒店是一個與無限有關的問題。
假設Hilbert是一個酒店的所有者,且該酒店有無限個房間。
一天一個大巴達到了Hilbert的酒店,假設大巴上有無限個旅客想要住在Hilbert的酒店,假設Hilbert的酒店有無限個房間,因此能夠容納無限個旅客。
但有一天有無限輛大巴達到了Hilbert的酒店,每輛大巴上載有無限個旅客,且所有大巴的旅客都要在酒店過夜。
Hilbert想了一會說"沒問題",然後他畫了一個數位三角形,然後把房間的鑰匙遞給沿著三角形對角線的所有乘客。通過這種方式,Hilbert能夠給每輛大巴的每個乘客一把唯一的鑰匙。
我們可以通過重複Hilbert的做法來解決Hilbert酒店問題。
例如,可以建立陣列的陣列來代表三角形的數位,然後遍歷該陣列的陣列來構建對角線並沿著對角線將房間鑰匙交給每輛大巴的乘客。文章末給出了這種方式的實現,當然也存在其他的非並行實現。下面換個角度看下這個問題。
遞迴並行演演算法中,每輛大巴所需的鑰匙位於三角形的最右側對角線。對角線中的鑰匙所在的位置等於三角形高度,這一點就是判定某把鑰匙是不是本大巴的關鍵。
假設Hilbert的酒店中有無限個僱員,每個僱員負責給一輛大巴的所有旅客發放鑰匙。負責第一輛大巴的僱員(Bus 1 Clerk)靠近負責第二輛大巴的僱員(Bus 2 Clerk),負責第二輛大巴的僱員(Bus 2 Clerk)靠近負責第三輛大巴的僱員(Bus 3 Clerk),以此類推。
此外還有一個僱員(Room Key Clerk),他負責將所有房間的鑰匙依次交給第一個僱員(Bus 1 Clerk),即房間一是第一把鑰匙,房間二是第二把鑰匙,房間三是第三把鑰匙,以此類推。
(從Room Key Clerk接收到鑰匙的)Bus 1 Clerk知道接收到的第一把鑰匙是給他負責的大巴的第一個乘客,因此Bus 1 Clerk會為第一輛巴士的第一位乘客準備一號房間的含鑰匙在內的歡迎禮包(welcome kit),此外他還知道接收到的第二把鑰匙不是給他負責的大巴的,而是給下一個僱員的,第三把鑰匙是給Bus 1的,因此由Bus 1 Clerk負責,但第四把和第五把鑰匙不是給Bus 1的,因此Bus 1 Clerk會將它們傳給下一個僱員。當歡迎禮包發放給Bus 1的所有乘客之後(此時需要假設顧客是有限的,防止程式無法結束),Bus 1 Clerk會將它們返還給Hilbert。後面將會看到,將鑰匙還給Hilbert是一個有用的步驟,可以並行實現最終的類似reduce的操作。
下一個僱員Bus 2 Clerk的行為和第一個僱員相同,他知道接收到的第一個鑰匙需要交給他負責的大巴的第一個乘客,而第二把鑰匙則需要交給下一個僱員,以此類推。
在某種程度上,由於我們的程式不能像原來的Hilbert問題那樣永遠繼續下去,因此需要能夠停止移交鑰匙,並通知所有僱員停止工作。此時需要將僱員準備的歡迎禮包還給Hilbert。最終,所有的僱員都會停止工作,並在Hilbert接收到準備好的歡迎禮包之後就可以通知Hilbert也停止工作。
這裡提供了一個Go實現的並行演演算法。使用goroutine來代表每一個角色,包括Hilbert和僱員。
下面是Hilbert的實現:
func Hilbert(upTo int) {
keysCh := make(chan int)
go RoomKeysClerk(upTo, keysCh) //1
make(chan []hilberthotel.WelcomeKit)
go BusClerk(1, keysCh, welcomeKitCh) //2
for envelope := range welcomeKitCh { //3
fmt.Println(envelope)
}
}
其程式碼比較簡單,入參upTo
表示房間鑰匙的上限:
keysch
,並使用keysch
作為引數來啟動Room Key Clerk的goroutine。welcomeKitCh
(僱員會從該channel中接收鑰匙,並在僱員結束工作後傳送歡迎禮包),用於接收Welcome Kits(歡迎禮包),並使用keysch
和welcomeKitCh
作為引數來啟動第一個BusClerk(大巴僱員)welcomeKitCh
迴圈接收僱員準備的禮包Room Key Clerk 的實現也很簡單,它通過keysCh
將鑰匙分發出去,在鑰匙到達上限upTo
之後,關閉keysCh
:
func RoomKeysClerk(upTo int, keysCh chan<- int) {
for i := 0; i < upTo; i++ {
keysCh <- i + 1
}
close(keysCh)
}
Bus Clert的實現要複雜一些:
func BusClerk(busNumber int, roomKeysCh <-chan int, welcomeKitsCh chan<- []hilberthotel.WelcomeKit, buffer int, delay time.Duration) { //1
var count = 0 //2
var keyPosition = 0
var nextClerkCh chan int
welcomeKits := []hilberthotel.WelcomeKit{}
for roomKey := range roomKeysCh {
if nextClerkCh == nil { //3
nextClerkCh = make(chan int, buffer)
go BusClerk(busNumber+1, nextClerkCh, welcomeKitsCh, buffer, delay)
}
if count == keyPosition { //4
kit := hilberthotel.NewWelcomeKit(busNumber, keyPosition, roomKey, delay)
welcomeKits = append(welcomeKits, kit)
keyPosition++
count = 0
continue
}
nextClerkCh <- roomKey
count++
}
if nextClerkCh != nil { //5
welcomeKitsCh <- welcomeKits
close(nextClerkCh)
} else {
close(welcomeKitsCh) //6
}
}
welcomeKitCh
用於在處理結束之後傳送歡迎禮包(welcomeKits
),roomKeysCh
用於讀取鑰匙號count
之後,使用一個變數keyPosition
來儲存下一個乘客的鑰匙位置,使用channel nextClerkCh
通知下一個BusClerk。通過迴圈讀取roomKeysCh
來啟動整個處理邏輯。nextClerkCh
為nil,之後會初始化nextClerkCh
並啟動下一個BusClerkcount
和keyPosition
,用於確定此時的鑰匙是給本大巴的還是給其他大巴的。當鑰匙是給本大巴的,它會建立一個WelcomeKit並將其儲存在它的集合中,反之,將鑰匙傳給下一個BusClerkroomKeysCh
被關閉),它會關閉用於通知下一個BusClerk的nextClerkCh
channelwelcomeKitCh
並通知Hilbert其將結束任務上述解決方案基於"相對獨立"的處理流程:
相對獨立
於Bus Clerk 1,這裡說的"相對獨立"並不是完全獨立,因為Room Key Clerk還需要等待Bus Clerk 1從keyCh
channel接收鑰匙(即使用了帶快取的 keysCh channel,緩衝區仍然會被填滿,從而迫使 Room Key Clerk 等待Bus Clerk 1從通道讀取資料)welcomeKitCh
channel中接收welcomeKits因此,我們可以說所有的Bus Clerk和Hilber執行的都是"相對獨立"的邏輯,需要通過調整channel的傳送/接收來達到期望的結果。
雖然我們的並行設計實現的方案很優雅,但它也帶來了如下開銷:
另一方面,這種設計的好處在於,如果在多核硬體上執行該並行演演算法,演演算法中固有的並行邏輯會帶來效能上的提升。
每個大巴僱員都有一些核心的工作需要完成,此外還有一些並行編排的工作(即設定goroutine和通過channel傳送/接收鑰匙)。
並行可以提升核心工作的效能。在Hilbert的例子中,核心工作就是為每個客戶準備歡迎禮包(即函數NewWelcomeKit執行的內容)。能夠並行執行的核心工作越多,就越能在相同的時間內服務更多的顧客。
為了實現並行處理,我們需要執行一些並行編排工作。與並行編排工作相比,核心工作占主導地位越高,從並行性中獲得的好處就越多。在Hilbert的例子中,每個大巴僱員的核心工作是準備歡迎禮包。因此,準備歡迎禮包的工作佔比越高,多核硬體上執行並行設計的效率就越高。另一方面,處理的顧客越多,並行編排工作也就越重(由於需要在更多的goroutines之間進行切換和傳送/接收鑰匙),因此並行的成本也會越高。
可以使用樣例程式碼提供的benchmarks,通過變更顧客數目來對效能進行驗證。
也可以使用上面所使用的並行方案的基本設計匯出非並行遞迴方案:
本文來自部落格園,作者:charlieroro,轉載請註明原文連結:https://www.cnblogs.com/charlieroro/p/17211356.html