自从赫伯-萨特(Herb Sutter)宣布 "免费午餐 "已经结束、"并发 "即将来临以来,13 年已经过去了,但是很难说大多数主流语言已经向并发编程模式做出了强有力的转变。我们不得不承认,并发就是很难,一些世界领先的编程语言所面临的挑战就是最好的证明。
不幸的是,大多数语言还没有摆脱线程与异步的二分法。你要么使用线程,要么使用单线程事件循环,再加上一堆花哨的装饰,让代码更容易接受。将线程与事件循环混合使用是可能的,但却非常复杂,很少有程序员能承受得起这样的心理负担。
线程在有良好库支持的语言中并不是一件坏事,其可扩展性也比十年前要好得多,但对于非常高的并发级别(~100,000 个线程及以上),线程仍然是不够的。另一方面,事件驱动编程模型通常是单线程的,不能很好地利用底层硬件。更令人不快的是,它们大大增加了编程模型的复杂性。我很喜欢鲍勃-尼斯特罗姆(Bob Nystrom)的《你的函数是什么颜色》(What Color is Your Function
)一书,书中解释了 "非阻塞模型 "是多么令人讨厌。其核心思想是,在异步模型中,我们必须在思想上注意到每个函数的阻塞性质,而这将影响我们从何处调用该函数。
Python 尝试了使用异步,这是如此复杂,以至于许多 Python 名人都承认他们不理解它,当然它也受到"函数颜色"问题的困扰,任何阻塞调用都会毁了你的一天。C + + 似乎正在朝着与 C + + 20协同程序建议类似的方向发展,但是 C + + 对用户隐藏魔力的能力远不如 Python,所以我预测它最终会得到一大堆模板,能够理解它们的人会更少。这里的基本问题是 Python 和 C + + 都试图在库级别上解决这个问题,而实际上它需要一个语言运行时解决方案。
What Go does right
正如你可能已经从本文的标题中猜到的那样,这就是 Go。我很高兴能公开宣称,Go 是真正做到了这一点的主流语言。它的核心设计依赖于两个关键原则:
- 跨内核无缝轻量级抢占式并发(Seamless light-weight preemptive concurrency across cores)
- CSP(通信顺序进程模型)和通过通信共享内存
这两项原则在 Go 中得到了很好的实现,与当今其他流行的编程语言相比,这两项原则的结合使 Go 中的并发编程成为迄今为止体验最好的编程语言。其主要原因是这两项原则都是在语言运行时实现的,而不是委托给库。
你可以将 goroutines 视为线程,这是一个相当不错的心理模型。它们是真正廉价的线程--因为 Go 运行时实现了它们的启动和切换,而无需依赖操作系统内核。在最近的一篇文章中,我测得我的机器上 goroutine 的切换时间为 ~170 ns,比线程切换时间快 10 倍。
但这不仅仅是切换时间的问题;Goroutine也有可以在运行时增长的小堆栈(线程堆栈不能做到这一点,传统意义上的栈被 runtime 独占了,goroutine 使用的栈实际上是在堆空间分配的),它也经过了仔细的调优,能够同时运行数百万个Goroutine。
这里并没有什么神奇之处;考虑一下这种说法--如果 C++、JS 或 Python 中的线程非常轻量级和快速,我们就不需要异步模型了。Go 就是这种情况。正如 Bob Nystrom 在他的文章中所说--Go 已经消除了同步代码和异步代码之间的区别(Go has eliminated the distinction between synchronous and asynchronous code.)。
然而,这还不是全部。第二条原则也至关重要。对线程的主要反对意见不仅仅是性能问题,还有正确性问题和复杂性问题。使用线程编程很难--很难在不造成死锁的情况下同步访问数据结构;很难推理多个线程访问相同数据的情况;很难选择正确的锁粒度,等等。
这就是 Go 的"通过通信进行共享"原则的用武之地。在惯用的 Go 程序中,您不会看到大量互斥体、条件变量和保护共享数据的关键区域。事实上,您可能根本不会看到太多锁定。这是因为 Go 鼓励程序员使用通道,并且通道内置于语言中,具有 select 等很棒的功能。正确使用通道可以消除对更显式锁定的需要,更容易正确编写、调整性能和调试。
此外,通过将这些功能构建到运行时中,Go 可以实现诸如竞争检测器(race detector)之类的出色工具,这使得并发错误更容易被排除。这一切都非常契合!显然,Go 中仍然存在并发编程的许多挑战 - 这些都是任何语言都无法消除的问题的本质复杂性;不过,Go 在消除附带的复杂性方面做得很好。
基于以上原因,我相信 Node.js 的创建者 Ryan Dahl 的观点是完全正确的:如果要构建服务器,我无法想象除了 Go 之外还能用什么。[......]我认为 Node 并不是构建大型服务器网络的最佳系统。为此,我会使用 Go。老实说,这就是我离开 Node 的原因。我意识到:哦,事实上,这并不是最好的服务器端系统。
不同的语言适合做不同的事情,这就是为什么程序员的武器库
中应该有几种足够不同的语言。如果并发性是您应用程序的核心,那么 Go 就是您应该使用的语言。