Go 部落格
Go 併發模式:超時與前進
併發程式設計有其自身的慣用語。一個很好的例子是超時。儘管 Go 的通道不直接支援超時,但它們很容易實現。假設我們想從通道 ch
接收值,但最多等待一秒鐘。我們可以先建立一個信令通道,並啟動一個 goroutine,該 goroutine 在向該通道傳送之前會休眠。
timeout := make(chan bool, 1)
go func() {
time.Sleep(1 * time.Second)
timeout <- true
}()
然後我們可以使用一個 select
語句從 ch
或 timeout
中接收。如果在等待一秒後 ch
沒有值到達,則會選擇 timeout 分支,並放棄從 ch 讀取的操作。
select {
case <-ch:
// a read from ch has occurred
case <-timeout:
// the read from ch has timed out
}
timeout
通道的緩衝區大小為 1,允許 timeout goroutine 傳送值到該通道然後退出。該 goroutine 並不知道(或關心)這個值是否被接收。這意味著如果 ch
在超時前接收到值,該 goroutine 也不會永遠掛起。timeout
通道最終會被垃圾回收器回收。
(在此示例中,我們使用 time.Sleep
來演示 goroutine 和通道的機制。在實際程式中,您應該使用 time.After
,這是一個返回一個通道並在指定持續時間後向該通道傳送值的函式。)
讓我們看看這種模式的另一個變體。在此示例中,我們有一個程式同時從多個複製的資料庫讀取資料。該程式只需要其中一個答案,並且應該接受最先到達的答案。
函式 Query
接受一個數據庫連線的切片和一個 query
字串。它並行查詢每個資料庫並返回接收到的第一個響應。
func Query(conns []Conn, query string) Result {
ch := make(chan Result)
for _, conn := range conns {
go func(c Conn) {
select {
case ch <- c.DoQuery(query):
default:
}
}(conn)
}
return <-ch
}
在此示例中,閉包執行非阻塞傳送,這是透過在 select
語句中使用帶有 default
分支的傳送操作實現的。如果傳送不能立即進行,則會選擇 default 分支。非阻塞傳送保證了迴圈中啟動的 goroutine 不會掛起。然而,如果結果在主函式到達接收之前到達,傳送可能會失敗,因為沒有接收方準備好。
這個問題是典型的教科書式的競態條件示例,但修復方法很簡單。我們只需要確保通道 ch
帶緩衝區(透過在 make 函式的第二個引數中新增緩衝區長度),從而保證第一次傳送有地方存放值。這樣可以確保傳送總是成功,並且無論執行順序如何,都會檢索到最先到達的值。
這兩個示例展示了 Go 表達 goroutine 之間複雜互動的簡單性。
下一篇文章:真正的 Go 專案:SmartTwitter 和 web.go
上一篇文章:Go Playground 簡介
部落格索引