地鼠宝宝的轶事奇闻之并发初探

Golang并发初探

并行与并发

并行与并发是两个不同的概念,区别在于:

  • 并行:一起执行

  • 并发:一起发生

并发的多个任务并不会同时执行,它们只是同时发生,然后分时执行。比如现在给你两个任务:洗碗和拖地。你可以先完成洗碗,然后完成拖地。这就是顺序执行。你也可以先洗一个碗,然后拖一会地,然后再去洗一个碗,仔拖一会地,如此往复进行。这便是并发,你同时开始洗碗和拖地的任务,但在同一个时刻你只能做其中一件事。但是如果你女朋友刚好来了,她帮你洗碗,你去拖地。这就是并行,同时开始,同时执行。

现在知道并行为什么那么难了吧。你怎么可能有女朋友?女朋友怎么可能做家务?

Go的并发过程

那么go的并发是并行的吗?这并不一定,严格来说,可能是并行,也可能是并发。go的优势在于它在语言级别支持并发,而不是第三方库。

在探索go的并发之前,首先记住四个概念:

  • 操作系统线程:是操作系统线程,不是go的线程,这一点很重要,切记。

  • 逻辑处理器

  • goroutine

  • 调度器

首先线程是用来干嘛的?一个线程是一个执行空间,它被操作系统调度来执行函数中的代码。说白了,线程就是用来执行代码的。

一个线程是一个执行空间,它被操作系统调度来执行函数中的代码。说白了,线程就是用来执行代码的。

逻辑处理器是一个新概念。它并不是正的处理器。逻辑处理器有一个本地运行队列,队列里装的是goroutine。在不深入的情况下,暂时可以把它看做是一个容器。

goroutine是go的并发单元。同样,在不深入的情况下,暂且认为它就是一段代码。

调度器用来调度goroutine。同样 它也有一个全局运行队列,里面装的也是goroutine。

goroutine如何执行

当一个goroutine被创建时,首先会被放到调度器的全局运行队列。然后由调度器分配给一个逻辑处理器,也即是将这个goroutine放到逻辑处理器的本地运行队列中。但是逻辑处理器并不是正的处理器,不能执行代码。于是调度器会创建一个系统线程,并将逻辑处理器绑定到该系统线程上。然后逻辑处理器就可以利用这个线程来执行它本地运行队列中的goroutine了。

goroutine调度

当正在运行的goroutine执行一个阻塞的系统调用时,系统线程和阻塞的goroutine都会从逻辑处理器上分离。系统线程继续阻塞,等待goroutine的返回。调度器会创建一个新线程,并绑定到逻辑处理器上。然后调度器从逻辑处理器的本地运行队列中选择一个goroutine来运行。当阻塞的goroutine恢复时,它会从新被放回本地运行队列,然后等待执行。而系统线程会被回收保存下来,以便之后还能使用。


当goroutine网络I/O调用时,情况会不同。此时只有goroutine会从逻辑处理器分离,并移到集成了网络轮询器的运行时。一旦轮询器指示网络读写操作就绪,对应的goroutine就会重新分配到逻辑处理器上来完成操作。

goroutine的并行与并发

从上面的调度过程可以看出,goroutine是通过逻辑处理器来执行的。显然,如果只有一个逻辑处理器无论怎么调度都是并发的,而不可能是并行。

在go1.5版本之前还真就只有一个逻辑处理器。不过在之后的版本中逻辑处理器的数量默认设置为机器CPU核的数量了。同时,我们也可以通过runtime包的GOMAXPROCS函数来自定义逻辑处理器的数量。当有多个逻辑处理器时,调度器会将goroutine平均分配到每个逻辑处理器上。

光是有多个逻辑处理器还不行。每个逻辑处理器都需要与一个系统线程绑定。那么go可以创建多少系统线程呢?默认最多可以创建10000个系统线程。你没看错,就是一万个!这相对于逻辑处理器来说,可以说是没有限制了。然而这个限制还可以通过runtime/debug包的SetMaxThreads函数来更改。

现在所有的软条件都具备了,要真正做到并行还需要一个硬条件:你的机器得有多个物理处理器。否则一切都是瞎扯淡。

现在你明白为什么在回答go是并行还是并发的问题上为什么不确定了吧。它们确实是都有可能的,所以goroutine依然宣称是并发编程。

那么问题来了

那么问题来了,上面的过程也太过于简单和理想化了。有很多问题没有考虑或者没有讲清楚。比如有多个逻辑处理时,某个逻辑处理器的本地运行队列空了怎么办?从阻塞中恢复的系统线程去哪里了?逻辑处理器要怎么被调度?甚至说逻辑处理器是个啥?

其实要将go的并发,本篇是很不负责任的。目的只是先说明它大概是个什么样子,其中的细节会在后续篇章中慢慢讲明白。


猜你喜欢

转载自blog.csdn.net/puss0/article/details/80587185