Golang 的语法和运行时直接内置了对并发的支持。
Golang 里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Golang 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。Golang 运行时的调度器是一个复杂的软件,能管理被创建的所有 goroutine 并为其分配执行时间。这个调度器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行 goroutine。调度器在任何给定的时间,都会全面控制哪个 goroutine 要在哪个逻辑处理器上运行。
Golang 的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP)的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。
10 000
一. mac的cpu处理器个数、核数、超线程查看CPU信息
其中
machdep.cpu.core_count 核数为8
machdep.cpu.thread_count cpu数量为16,使用了超线程技术
二. 进程、线程、协程进程
当运行一个应用程序的时候,操作系统会为这个应用程序启动一个进程。可以将这个进程看作一个包含了应用程序在运行中需要用到和维护的各种资源的容器。这些资源包括但不限于内存地址空间、文件和设备的句柄以及线程。
线程
一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。每个进程至少包含一个线程,每个进程的初始线程被称作主线程。因为执行这个线程的空间是应用程序的本身的空间,所以当主线程终止时,应用程序也会终止。操作系统将线程调度到某个处理器上运行,这个处理器并不一定是进程所在的处理器。下图展示了一个运行中的应用程序的进程和线程视图
三. 逻辑处理器与本地运行队列1. 逻辑处理器
Golang 的运行时会在逻辑处理器上调度 goroutine 来运行。每个逻辑处理器都与一个操作系统线程绑定。在 Golang 1.5 及以后的版本中,运行时默认会为每个可用的物理处理器分配一个逻辑处理器。
2. 本地运行队列
每个逻辑处理器有一个本地运行队列。如果创建一个 goroutine 并准备运行,这个 goroutine 首先会被放到调度器的全局运行队列中。之后,调度器会将全局运行队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到被分配的逻辑处理器执行。
下图展示了操作系统线程、逻辑处理器和本地运行队列之间的关系
有时,正在运行的 goroutine 需要执行一个阻塞的系统调用,如打开一个文件。当这类调用发生时,线程和 goroutine 会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。与此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个 goroutine 来运行。一旦被阻塞的系统调用执行完成并返回,对应的 goroutine 会放回到本地运行队列,而之前的线程会保存好,以便之后可以继续使用
如果一个 goroutine 需要做一个网络 I/O 调用,流程上会有些不一样。在这种情况下,goroutine 会和逻辑处理器分离,并移到集成了网络轮询器的运行时。一旦该轮询器指示某个网络读或者写操作已经就绪,对应的goroutine 就会重新分配到逻辑处理器上来完成操作。
注意:Golang 运行时默认限制每个程序最多创建10000个线程。这个限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。如果程序试图使用更多的线程,就会崩溃
四. 并发与并行并发(concurrency)与并行(parallelism)不同。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了(Golang 的并发通过切换多个线程达到减少物理处理器空闲等待的目的)。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种 "使用较少的资源做更多的事情" 的哲学,也是指导 Golang 设计的哲学。
如果希望让 goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕 Golang 运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。下图展示了在一个逻辑处理器上并发运行 goroutine 和在两个逻辑处理器上并行运行两个并发的 goroutine 之间的区别
五. 一个进程最多能创建多少个线程?在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址 空间的范围也不同。比如最常⻅的 32 位和 64 位系统,如下所示:
通过这里可以看出:
- 32 位系统的内核空间占用 1G ,位于最高处,剩下的 3G 是用户空间;
- 64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的
在前面我们知道,在 32 位 Linux 系统里,一个进程的虚拟空间是 4G,内核分走了1G,留给用户用的只有 3G。
那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是我们可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。
如果想使得进程创建上千个线程,那么我们可以调整创建线程时分配的栈空间大小,比如调整为 512k:
简单总结下:
- 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
- 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制