Go 部落格

Go 並行模式:計時結束,繼續進行

Andrew Gerrand
2010 年 9 月 23 日

並行程式設計有其慣用語法。超時便是個很好的例子。儘管 Go 的通道不直接支援超時,但它們很容易實作。假設我們想從通道 ch 接收,但只願意等候值最多一秒到來。我們會從建立信號通道和發起一個睡後才會在通道上傳送的 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 通道會暫存在一個空間為 1 值的緩衝區中,允許超時 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
}

在此範例中,封閉會做非阻擋傳送,它是透過使用 select 語句中的 send 操作搭配一個 default 案例來達成。如果傳送無法立刻完成,就會選取預設案例。讓傳送變成非阻擋可以確保迴圈中啟動的所有 goroutine 都不會閒置。不過,如果結果在主函式接收結果前就已到達,傳送可能會失敗,因為沒有人已準備好。

這個問題是教科書上所說的 競爭條件 的範例,不過解決方法很簡單。我們只要確定緩衝頻道 ch (在呼叫 make 時將緩衝長度新增為第二個引數),即可保證第一個傳送有地方放置值。這會確保傳送必定會成功,並且將會擷取最早到達的第一個值,不論執行順序為何。

這兩個範例說明了 Go 能夠多麼簡潔地表達 goroutine 之間的複雜互動。

下一篇文章: 真正的 Go 專案:SmartTwitter 和 web.go
前一篇文章: 推出 Go Playgrounds
部落格索引