并发与并行的区别是什么?
让我们先来学习一下抽象程度较高的概念:什么是操作系统的线程(thread)和进程(process)。这会有助于后面理解Go语言运行时调度器如何利用操作系统来并发运行goroutine。当运行一个应用程序(如一个IDE或者编辑器)的时候,操作系统会为这个应用程序启动一个进程。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。
图6-1展示了一个包含所有可能分配的常用资源的进程。这些资源包括但不限于内存地址空间、文件和设备的句柄以及线程。一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。每个进程至少包含一个线程,每个进程的初始线程被称作主线程。因为执行这个线程的空间是应用程序的本身的空间,所以当主线程终止时,应用程序也会终止。操作系统将线程调度到某个处理器上运行,这个处理器并不一定是进程所在的处理器。不同操作系统使用的线程调度算法一般都不一样,但是这种不同会被操作系统屏蔽,并不会展示给程序员。
图6-1 一个运行的应用程序的进程和线程的简要描绘
操作系统会在物理处理器上调度线程来运行,而 Go 语言的运行时会在逻辑处理器上调度goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。在1.5版本①上,Go语言的运行时默认会为每个可用的物理处理器分配一个逻辑处理器。在1.5版本之前的版本中,默认给整个应用程序只分配一个逻辑处理器。这些逻辑处理器会用于执行所有被创建的goroutine。即便只有一个逻辑处理器,Go也可以以神奇的效率和性能,并发调度无数个goroutine。
在图6-2中,可以看到操作系统线程、逻辑处理器和本地运行队列之间的关系。如果创建一个goroutine并准备运行,这个goroutine就会被放到调度器的全局运行队列中。之后,调度器就将这些队列中的goroutine分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的goroutine会一直等待直到自己被分配的逻辑处理器执行。
图6-2 Go调度器如何管理goroutine
有时,正在运行的goroutine需要执行一个阻塞的系统调用,如打开一个文件。当这类调用发生时,线程和goroutine会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。与此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个goroutine来运行。一旦被阻塞的系统调用执行完成并返回,对应的goroutine会放回到本地运行队列,而之前的线程会保存好,以便之后可以继续使用。
如果一个goroutine需要做一个网络I/O调用,流程上会有些不一样。在这种情况下,goroutine会和逻辑处理器分离,并移到集成了网络轮询器的运行时。一旦该轮询器指示某个网络读或者写操作已经就绪,对应的goroutine就会重新分配到逻辑处理器上来完成操作。调度器对可以创建的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建10 000个线程。这个限制值可以通过调用runtime/debug包的SetMaxThreads方法来更改。如果程序试图使用更多的线程,就会崩溃。
并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导Go语言设计的哲学。
如果希望让goroutine并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将goroutine平等分配到每个逻辑处理器上。这会让goroutine在不同的线程上运行。不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕Go语言运行时使用多个线程,goroutine依然会在同一个物理处理器上并发运行,达不到并行的效果。
图6-3展示了在一个逻辑处理器上并发运行goroutine和在两个逻辑处理器上并行运行两个并发的goroutine之间的区别。调度器包含一些聪明的算法,这些算法会随着Go语言的发布被更新和改进,所以不推荐盲目修改语言运行时对逻辑处理器的默认设置。如果真的认为修改逻辑处理器的数量可以改进性能,也可以对语言运行时的参数进行细微调整。后面会介绍如何做这种修改。
图6-3 并发和并行的区别
答案出自《Go语言实战》
本书是写给已经有一定其他语言编程经验,并且想学习Go语言的中级开发者的。我们写这本书的目的是,为读者提供一个专注、全面且符合语言习惯的视角。我们同时关注语言的规范和实现,涉及的内容包括语法、类型系统,并发、通道、测试以及其他一些主题。我们相信,对于刚开始学Go语言的人,以及想要深入了解这门语言内部实现的人来说,本书都是极佳的选择。
内容概览
本书由9章组成,每章内容简要描述如下。
- 第1章快速介绍这门语言是什么,为什么要创造这门语言,以及这门语言要解决什么问题。这一章还会简要介绍一些Go语言的核心概念,如并发。
- 第2章引导你完成一个完整的Go程序,并教你Go作为一门编程语言必须提供的特性。
- 第3章介绍打包的概念,以及搭建Go工作空间和开发环境的最佳实践。这一章还会展示如何使用Go语言的工具链,包括获取和构建代码。
- 第4章展示Go语言内置的类型,即数组、切片和映射。还会解释这些数据结构背后的实现和机制。
- 第5章详细介绍Go语言的类型系统,从结构体类型到具名类型,再到接口和类型嵌套。这一章还会展示如何综合利用这些数据结构,用简单的方法来设计结构并编写复杂的软件。
- 第6章深入展示Go调度器、并发和管道是如何工作的。这一章还将介绍这个方面背后的机制。
- 第7章基于第6章的内容,展示一些实际开发中用到的并发模式。你会学到为了控制任务如何实现一个goroutine池,以及如何利用池来复用资源。
- 第8章对标准库进行探索,深入介绍3个包,即log、json和io。这一章专门介绍这3个包之间的某些复杂关系。
- 第9章以如何利用测试和基准测试框架来结束全书。读者会学到如何写单元测试、表组测试以及基准测试,如何在文档中增加示例,以及如何把这些示例当作测试使用。