Concurrency is not Parallelism
前言
最近在看《Go语言精进之路 从新手到高手的编程思想、方法和技巧1》一书,其中提到了 Go 语言之父 Rob Pike 的一句关于并发和并行的话:"并发不是并行,并发关乎结构,并行关乎执行",值得我们深入思考。
- 相关的 talk slide:https://go.dev/talks/2012/waza.slide。
Rob 说并发和并行
我们先来看看 Go 之父 Rob Pike 是怎么解释并发和并行的。
Concurrency:Programming as the composition of independently executing processes. Processes in the general sense, not Linux processes. Famously hard to define.
Parallelism:Programming as the simultaneous execution of (possibly related) computations.
Concurrency vs Parallelism:
- Concurrency is about dealing with lots of things at once.
- Parallelism is about doing lots of things at once.
- Not the same, but related.
- Concurrency is about structure, parallelism is about execution.
- Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.
感兴趣的话,可以自己翻译一下,整段话中,比较核心的是最后一句,并发提供了一种构建解决方案的方法,以解决可能(但不一定)可以并行化的问题。
那么我自己的理解就是,当我们拿到一个编程问题时,我们可以通过合理的方案设计将解决该问题的解决方案做出 结构 化的拆分,如果我们将拆分之后的程序跑在多核 CPU 上,那么就可以并行 执行。
这里,所谓 结构 化的拆分,另一种理解方式就是 逻辑上的同时发生,与之对应的,并行就是 物理上的同时发生。
举个例子
我们考虑一个比较简单的例子,我给你一个序列,返回序列中每个数的平方数。
当然,一个很简单的实现如下:
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
calc(nums)
fmt.Println(nums)
}
func calc(nums []int) {
for i := range nums {
nums[i] = nums[i] * nums[i]
}
}
你也看到了这其实是串行的计算,下面我们通过“并发”来做结构化的拆分:
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 4, 5}
rsChan := make(chan int, len(nums))
defer close(rsChan)
for _, num := range nums {
go calc(num, rsChan)
}
for range nums {
fmt.Println(<-rsChan)
}
}
func calc(num int, rsChan chan<- int) {
rsChan <- num * num
}
这样我们就实现了结构上的“并发”,因为每一个数字的计算都是开启了新的协程,当然,还看不出来好处,那如何计算结果很耗时呢?
来看一个测试:
package main
import (
"fmt"
"time"
)
func main() {
nums0 := []int{1, 2, 3, 4, 5}
nums1 := []int{1, 2, 3, 4, 5}
calc0(nums0)
calc1(nums1)
}
func calc0(nums []int) {
start := time.Now()
rsChan := make(chan int, len(nums))
defer close(rsChan)
for _, num := range nums {
go doCalc(num, rsChan)
}
for range nums {
_ = <-rsChan
}
fmt.Println("耗时: " + time.Since(start).String())
}
func doCalc(num int, rsChan chan<- int) {
time.Sleep(1 * time.Second)
rsChan <- num * num
}
func calc1(nums []int) {
start := time.Now()
for i := range nums {
time.Sleep(1 * time.Second)
nums[i] = nums[i] * nums[i]
}
fmt.Println("耗时: " + time.Since(start).String())
}
输出:
耗时: 1.0005136s
耗时: 5.0028766s
此外,我的机器是多核的,所以可以实现并行,如果是单核机器,那就只有并发性了。
总结
那么,总的来说,理解并行和并发主要是三点:
- 并发关乎结构,并行关乎执行。
- 并发提供了一种构建解决方案的方法,以解决可能(但不一定)可以并行化的问题。
- 并发是逻辑上的同时发生,并行是物理上的同时发生。
最后提出一个问题,Go 的协程和 Java 的多线程一定是并发还是并行?
如果使用了协程或者多线程,那么程序一定具有并发性,但是否具有并行性还要看物理机器是否是多核 CPU。