Go 博客
通过通信共享内存
传统的线程模型(例如,在编写 Java、C++ 和 Python 程序时通常使用)要求程序员使用共享内存在线程之间进行通信。通常,共享数据结构由锁保护,线程将争用这些锁以访问数据。在某些情况下,通过使用线程安全的数据结构(例如 Python 的 Queue)可以简化此过程。
Go 的并发原语 - goroutine 和 channel - 提供了一种优雅且独特的构建并发软件的方法。(这些概念有一个有趣的历史,起源于 C. A. R. Hoare 的通信顺序进程。)Go 不鼓励显式使用锁来协调对共享数据的访问,而是鼓励使用 channel 在 goroutine 之间传递数据引用。这种方法确保在任何给定时间只有一个 goroutine 可以访问数据。该概念在文档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 轮询逻辑(它本身只有几行),也不会优雅地处理耗尽 Resources 池的情况。
让我们看看使用 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通过通信共享内存。