通过通信来共享内存

创建于 2023年12月9日修改于 2024年5月5日
Go共享内存

原文链接: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