你可能会也,可能不会发现所有这些引人入胜的东西,但如果你正在阅读本书,你就有难以解决的问题,并且你想知道为什么这些问题很重要。 Go的做法有何不同,使它在并发性方面与其他流行语言不同?
我们在”并发与并行”章节提到,语言在操作系统线程和内存访问同步级别结束其抽象链是很常见的。Go采用不同的路线,用goroutines和channel的概念取而代之。
如果我们在抽象并发代码的两种方式中比较概念,我们可能会将goroutine与线程和通道相比较(这些基元只具有相似性,但希望能够进行比较帮助你获得更深入的理解)。 这些不同的抽象为我们做了什么?
Goroutines使我们不必从并行的角度思考我们的问题,而是让我们对问题进行模拟,使其更接近自然。 虽然我们讨论了并发和并行之间的区别,但这种差异如何影响我们对解决方案的建模可能并不明确。 我们来看一个例子。
比方说,我需要构建一个Web服务器,在客户端提交访问请求。 暂时搁置框架,用一种只提供线程抽象的语言,我可能会反思下列问题:
- 我的语言是否自然地支持线程,还是必须选择一个库?
- 我的线程限制边界应该在哪里?
- 此操作系统中的线程有多重?
- 我的程序将如何在处理线程中运行不同的操作系统?
- 我应该创建一个线程池来限制我创建的线程数量。 我如何找到最佳的数字?
所有这些都是需要考虑的重要事情,但是没有一个直接关注你正在尝试解决的问题。 你被放到了如何解决并行性问题的技术上。
如果我们退后一步并考虑原始问题,我们可以这样说:个人用户正在连接到我的终端并开启会话。 会话应该对他们的请求返回响应。 在Go中,我们几乎可以用代码直接表示这个问题:我们将为每个传入连接创建一个goroutine,在那里将请求放在那里(可能与其他数据/服务的goroutines进行通信),然后从 goroutine的函数返回信息。使用Go可以让我们自然地思考这个问题并直接映射到编码。
这是通过Go对我们的承诺实现的:goroutines是轻量级的,我们通常不必担心创建一个导致耗费很多资源。有时候需要考虑系统中有多少个goroutines正在运行,但是这么做是一个过早的优化。 将此与线程进行对比,你可以预先考虑这些问题。
不过这并不意味着这种对并发问题建模不重要。在使用Go的情况下,语言是围绕并发设计的,所以这种语言与它提供的并发原语是一致的。 这意味着更少的摩擦和更少的错误。
对问题的更直观自然的映射处理好处是巨大的,它也有一些有益的副作用。Go的运行时自动将goroutine多路复用到OS线程,并为我们管理其调度。 这意味着可以在不需要改变我们如何模拟问题的情况下对运行时进行优化; 这是经典的关注点分离。 随着并行性的进步,Go的运行时在更新,程序的性能也在提高。 留意Go的发布说明,偶尔你会看到类似的东西:
在Go 1.5中,goroutines的调度顺序已经改变。
并发和并行的分离还有另一个好处:由Go的运行时为你管理goroutines的调度,它可以检查像阻塞等待I/O的goroutines和智能地重新分配OS线程到未阻塞的goroutines之类的事情。 这也增加了你的代码的性能。我们将在第六章中更多地讨论Go的运行时为你做什么。
现实问题和Go代码之间更自然映射的另一个好处是提高了以并发方式建模的问题数量。 由于我们作为开发人员所面临的问题在现实中是并发的,使用Go可以在更细的粒度级别上编写并发代码,而不是我们在其他语言中可能会使用的思考方式;例如,回到我们的Web服务器示例,我们现在将为每个用户建立一个goroutine,而不是将多个连接复用到一个线程池中。 这种更精细的粒度使程序能够在主机上实现友好的并行扩展。
goroutine是Go提供的解决方案的一部分,从CSP概念衍生出的通道——channel和select语句同样有用。
例如,通道本身可与其他通道合并。 这使得编写大型系统变得更简单,因为莫可以通过组合输出来协调多个子系统的输入。 可以将输入通道与超时,执行取消或把消息组合到其他子系统。 与之相对应的,协调互斥是一个值得关注的话题。
select语句是Go的通道的补充,并且是赋予通道巨大威力的直接因素。 select语句允许你有效地等待事件,以统一的随机方式从竞争渠道中选择消息,如果没有消息等待,则继续操作。
这些由CSP和支持它的运行时所激发的奇妙基元就是Go的动力。我们将在这本书的其余部分来发现这些东西是如何工作的,为什么以及如何使用它们来编写出色的代码。