Go的并发哲学
发布者:admin 发表于:417天前 阅读数:540 评论:0

CSP过去和现在都是Go设计的重要组成部分; 然而,Go还支持通过内存访问同步和遵循该技术的基元来编写并发代码的更传统手段。 同步和其他软件包中的结构和方法允许你执行锁定,创建资源池,抢占goroutine等。

这些能力对你来说将非常有用,因为它可以让你对选择编写哪种类型的并发代码来解决问题拥有更大的自由度,但它也可能有点混乱。 该语言的新手经常会得到这样的印象:并发CSP风格被认为是在Go中编写并发代码的唯一方法。 例如,在同步软件包的文档中,它说:

sync包提供基本的同步原语,如互斥锁。 除了Once和WaitGroup类型之外,大部分类型都是供底层库例程使用的。 通过通道和通信可以更好地完成更高级别的信息交互。

在官方FAQ文档中提到:

关于互斥锁,sync包实现它们,但我们希望Go编程风格将鼓励人们尝试更高级的技术。 特别是,考虑构建你的程序,这样一次只有一个goroutine负责某个特定的数据。不要通过共享内存进行通信。 相反,通过通信共享内存。

还有很多文章,讲座和采访,Go核心库大多都支持CSP风格,比如sync.Mutex。

因此,Go团队为什么选择公开内存访问同步原语会感到困惑是完全可以理解的。 更令人困惑的是,你会看到通常会出现的同步原语,看到人们抱怨过度使用通道,并且还听到一些Go团队成员表示可以使用它们。 这是来自Go Wiki的关于此事的引文:

Go的格言之一是“通过沟通共享内存,不要通过共享内存进行通信”。也就是说,Go确实在sync包中提供了传统的锁定机制。 大多数锁定问题都可以使用通道或传统锁来解决。那么你应该使用哪个?使用最具表现力和/或最简单的。

这是很好的建议,这是你在使用Go时经常看到的指导方针,但它有点含糊。 我们如何理解什么更具表现力和/或更简单? 我们可以使用什么标准? 幸运的是,可以使用一些指导来帮助我们做正确的事情。 正如我们将会看到的那样,主要区分的方式来自于试图管理并发性的地方:从内部到紧密的范围,或者在整个系统中。 下图列举了这些指标:

让我们一步接一步的看这幅图:

你想转移数据的所有权吗?

如果你有一些代码能够产生结果,并希望与另一部分代码共享这个结果,那么你真正在做的是转移那些数据的所有权。 如果你熟悉不支持垃圾回收的语言的内存所有权概念,那么也可以称之为:数据所有者,使并发程序安全的一种方法是确保只有一个并发上下文拥有数据的所有权 。 通道可以帮助我们来传达这一概念。

这样做的一大好处是你可以创建缓冲通道来实现资源廉价的内存队列,从而将你的生产者与消费者分离。 另一个是通过使用通道,你可以隐式地将你的并发代码与其他并发代码组合在一起。

你是否试图保护结构的内部状态?

这是内存访问同步原语的一个很好的选择,也是一个非常强大的指示器,你不应该使用通道。 通过使用内存访问同步原语,你可以隐藏从呼叫者锁定关键部分的实现细节,但不会给调用者带来复杂性。 这是一个线程安全类型的小例子:

type Counter struct {
    mu sync.Mutex value int
}
func(c *Counter) Increment()
{
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

如果你回想一下原子性的概念,我们可以说在这里所做的是定义了Counter类型的原子性范围。 调用增量可以被认为是原子的。

记住这里的关键词是”内部的”。 如果你发现自己超出锁定范围,这应该会引起重视。 尽量将锁限制在一个小的范围内。

你是否想要协调多个逻辑?

请记住,通道本质上比内存访问同步基元更具可组合性。 将锁分散在各个结构中听起来像是一场噩梦。

如果你因Go的选择语句而使用通道,且能够充当队列安全地传递,你会发现控制软件中出现的紧急复杂性要容易得多。 如果你发现自己在努力了解并发代码的工作原理,为什么会发生死锁或竞争,并且你正在使用基元,这可能是你需要切换到通道的一个很好的信号。

这是一个性能的关键部分吗?

这绝对不意味着,“我希望我的程序是高性能的,因此我只会使用互斥锁。”相反,如果你有一部分程序是已经分析过的,并且事实证明它是一个主要的瓶颈,当你发现这里比程序的其余部分慢一些,使用内存访问同步原语可以帮助这个关键部分在负载下执行。由于通道使用内存访问同步来操作,因此它们只能更慢。

希望这可以清楚地说明是否利用CSP风格的并发或内存访问同步。 还有其他一些模式和做法在使用操作系统线程作为抽象并发的方式的语言中很有用。 例如,像线程池这样的东西经常出现。 因为这些抽象的大部分是针对OS线程的优点和缺点的,所以使用Go时的一个很好的经验法则是放弃这些模式。

这并不是说它们根本没有用处,而是Go中的用例受到了更多限制。 坚持用goroutines为你的问题建模,用它们来代表你的工作流程的并发部分,并且不要害怕在启动它们时变得自由。 你很可能需要重新构建你的程序,而不是关注你的硬件可以支持多少个goroutines的上限。

Go的并发理念可以这样概括:为了简单起见,在可能的情况下使用通道,并且像免费资源一样处理goroutine(而不需要过多过早的考虑资源占用情况)。