Go 博客
通过通信共享内存
传统的线程模型(例如,在编写 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 的声明语法
博客索引