通过通信共享内存

247 阅读2分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

传统的线程模型(例如,通常在编写 Java、C++ 和 Python 程序时使用)要求程序员使用共享内存在线程之间进行通信。 通常,共享数据结构受锁保护,线程将争用这些锁来访问数据。 在某些情况下,这可以通过使用线程安全的数据结构(例如 Python 的队列)来简化。

Go 的并发原语——goroutine 和通道——提供了一种优雅而独特的方式来构建并发。 (这些概念有一段有趣的历史,始于 C. A. R. Hoare 的 Communicating Sequential Processes。)Go 鼓励使用通道在 goroutine 之间传递对数据的引用,而不是显式地使用锁来调解对共享数据的访问。 这种方法确保在给定时间只有一个 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 轮询逻辑(它本身只有几行),也不会优雅地处理资源池的耗尽。

让我们看一下使用 Go 实现的相同功能。 在这个例子中,Poller 是一个函数,它从输入通道接收要轮询的资源,并在完成后将它们发送到输出通道。

type Resource stringfunc Poller(in, out chan *Resource) {
    for r := range in {
        // poll the URL
​
        // send the processed Resource to out
        out <- r
    }
}