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 接收要轮询的 Resources,并在完成后将它们发送到输出 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 程序的演练,请参阅 Codewalk 通过通信共享内存

下一篇文章: Defer、Panic 和 Recover
上一篇文章: Go 的声明语法
博客索引