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 上沒有收到任何資料,則會選擇超時情況,讀取 ch 的嘗試將被放棄。

select {
case <-ch:
    // a read from ch has occurred
case <-timeout:
    // the read from ch has timed out
}

timeout 通道是帶有一個值為緩衝的,允許超時 goroutine 傳送到通道然後退出。該 goroutine 不知道(也不關心)值是否已被接收。這意味著,如果 ch 的接收發生在超時到達之前,該 goroutine 將不會永遠掛起。timeout 通道最終將被垃圾收集器回收。

(在此示例中,我們使用了 time.Sleep 來演示 goroutine 和通道的機制。在實際程式中,您應該使用 [time.After](/pkg/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
}

在此示例中,閉包執行非阻塞傳送,它透過使用帶有 default 情況的 select 語句中的傳送操作來實現。如果傳送無法立即進行,將選擇預設情況。使傳送非阻塞可確保在迴圈中啟動的任何 goroutine 都不會掛起。但是,如果結果在主函式到達接收之前到達,則傳送可能會失敗,因為沒有人準備好接收。

此問題是眾所周知的 競爭條件 的典型例子,但修復非常簡單。我們只需確保緩衝通道 ch(透過將緩衝區長度作為第二個引數新增到 make),從而保證第一次傳送都有一個地方可以存放值。這確保了傳送將始終成功,並且無論執行順序如何,第一個到達的值都將被檢索。

這兩個示例演示了 Go 在表達 goroutine 之間複雜互動時的簡單性。

下一篇文章:真實 Go 專案:SmartTwitter 和 web.go
上一篇文章:Go Playground 簡介
部落格索引