通过通信来共享内存
原文链接:https://go.dev/blog/codelab-share (作者: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》文档中有所总结:
不要通过共享内存来通信;相反,通过通信来共享内存。
(Do not communicate by sharing memory; instead, share memory by communicating.)
假设有一个轮询 URL 列表的程序。在传统的线程环境中,可以这样组织其数据:
type Resource struct {
url string
polling bool
lastPolled int64
}
type Resources struct {
data []*Resource
lock *sync.Mutex
}
然后,一个 Poller
函数可能如下所示:
func Poller(res *Resources) {
for {
// 获取最近最少被轮询的资源,标记为正在轮询
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
}
// 轮询 URL
// 更新资源的 polling 和 lastPolled 字段
res.lock.Lock()
r.polling = false
r.lastPolled = time.Nanoseconds()
res.lock.Unlock()
}
}
这个函数有大约一页那么长,还需要更多的细节才能使其完整。它甚至不包括 URL 轮询逻辑(这本身只会是几行),也不能优雅地处理资源池用尽的情况。
让我们看看使用 Go 语言习语实现相同功能的代码。在这个例子中,Poller
是一个函数,它从输入通道接收要轮询的 Resources,并在完成时将它们发送到输出通道。
type Resource string
func Poller(in, out chan *Resource) {
for r := range in {
// 轮询 URL
// 发送 Resource 到输出通道
out <- r
}
}
前面示例中的繁琐逻辑显然消失了,我们的 Resource 数据结构也不再包含并发相关的非业务数据。所有剩下的都是重要的部分。这组对比应该让你对 Go 语言特性的强大之处有所了解。
上面的代码片段中有许多省略。有关使用这些思想的完整、地道的 Go 程序讲解,请参阅此链接 Share Memory By Communicating。