Go 部落格

Go 併發模式:超時與前進

Andrew Gerrand
2010 年 9 月 23 日

併發程式設計有其自身的慣用語。一個很好的例子是超時。儘管 Go 的通道不直接支援超時,但它們很容易實現。假設我們想從通道 ch 接收值,但最多等待一秒鐘。我們可以先建立一個信令通道,並啟動一個 goroutine,該 goroutine 在向該通道傳送之前會休眠。

timeout := make(chan bool, 1)
go func() {
    time.Sleep(1 * time.Second)
    timeout <- true
}()

然後我們可以使用一個 select 語句從 chtimeout 中接收。如果在等待一秒後 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 簡介
部落格索引