Go 部落格

透過通訊共享記憶體

Andrew Gerrand
2010 年 7 月 13 日

傳統的執行緒模型(例如,在編寫 Java, C++ 和 Python 程式時常用)要求程式設計師使用共享記憶體來線上程間進行通訊。通常,共享資料結構會受到鎖的保護,執行緒會爭奪這些鎖來訪問資料。在某些情況下,使用執行緒安全的資料結構(例如 Python 的 Queue)可以簡化這一過程。

Go 的併發原語——goroutines 和 channels——提供了一種優雅且獨特的併發軟體構建方式。(這些概念有有趣的淵源,始於 C. A. R. Hoare 的通訊順序程序(Communicating Sequential Processes))。Go 鼓勵使用 channels 在 goroutines 之間傳遞資料引用,而不是顯式地使用鎖來協調對共享資料的訪問。這種方法確保在給定時間只有一個 goroutine 可以訪問資料。這一概念在文件《Effective Go》(所有 Go 程式設計師的必讀之作)中被總結為:

不要透過共享記憶體來通訊;相反,透過通訊來共享記憶體。

考慮一個輪詢 URL 列表的程式。在傳統的執行緒環境中,其資料結構可能會像這樣組織:

type Resource struct {
    url        string
    polling    bool
    lastPolled int64
}

type Resources struct {
    data []*Resource
    lock *sync.Mutex
}

然後,一個 Poller 函式(其中許多函式將執行在單獨的執行緒中)可能會看起來像這樣:

func Poller(res *Resources) {
    for {
        // get the least recently-polled Resource
        // and mark it as being polled
        res.lock.Lock()
        var r *Resource
        for _, v := range res.data {
            if v.polling {
                continue
            }
            if r == nil || v.lastPolled < r.lastPolled {
                r = v
            }
        }
        if r != nil {
            r.polling = true
        }
        res.lock.Unlock()
        if r == nil {
            continue
        }

        // poll the URL

        // update the Resource's polling and lastPolled
        res.lock.Lock()
        r.polling = false
        r.lastPolled = time.Nanoseconds()
        res.lock.Unlock()
    }
}

這個函式大約有一頁長,並且需要更多細節才能完整。它甚至還沒有包含 URL 輪詢邏輯(這本身只有幾行),也無法優雅地處理 Resource 池耗盡的情況。

讓我們看看使用 Go 慣用法實現相同功能的示例。在這個示例中,Poller 是一個函式,它從輸入 channel 接收待輪詢的 Resource,並在完成後將它們傳送到輸出 channel。

type Resource string

func Poller(in, out chan *Resource) {
    for r := range in {
        // poll the URL

        // send the processed Resource to out
        out <- r
    }
}

上一個示例中那些精細的邏輯在這裡明顯不見了,我們的 Resource 資料結構也不再包含簿記資料。事實上,剩下的只是重要的部分。這應該能讓您稍微瞭解這些簡單語言特性所蘊含的力量。

上面的程式碼片段省略了許多內容。要了解一個使用這些思想的完整、地道的 Go 程式,請參閱程式碼演示 《透過通訊共享記憶體》

下一篇文章:Defer, Panic, and Recover
上一篇文章:Go 的宣告語法
部落格索引