一. 并发概念
1. 多核CPU
- 单核CPU主频接近
4GHz
时遇到瓶颈(能耗和散热
),所以2005年4月Intel推出第一次双核
奔腾CPU。 - 多核CPU就是在一枚处理器中集成两个或以上
计算引擎(内核)
,以线程方式
执行多任务。 单芯片多处理器
即CMP,他分为同构
和异构
两类,异构如CPU
和GPU
的组合。- CMP通过
总线共享的Cache结构
或片上的互连结构
进行数据共享与同步
,如下图:
查看CPU
# 查看CPU信息(Linux)
$ cat /proc/cpuinfo
# 查看CPU信息(Mac)
$ sysctl machdep.cpu|grep count
CPU参考实例:
型号名称: MacBook Pro
型号标识符: MacBookPro14,1
处理器名称: Intel Core i5
处理器速度: 2.3 GHz
处理器数目: 1
核总数: 2
L2 缓存(每个核): 256 KB
L3 缓存: 4 MB
内存: 8 GB
Boot ROM 版本: MBP141.0173.B00
SMC 版本(系统): 2.43f6
序列号(系统): FVFXR5KXXXXX
硬件 UUID: D1589F05-DF5C-5BEF-9943-XXXXXXXXXX
2. 进程与线程
- 进程:
资源分配的最小单位
,进程由控制块(PCB)、程序和数据组成,生命周期:执行-就绪-阻塞。 - 线程:
cpu调度的最小单位
,由进程创建,继承了进程的部分资源,具有进程的一些基本特征。
3. 并行与并发
- 并行(
parallelism
):多个
处理器上同时执行
,同时刻
。 - 并发(
concurrency
):同一
处理器上轮转执行
1,同时段
。
4. 串行与并发
- 串行执行:
顺序执行
,资源独占
,速度无关
,结果确定
- 并发执行:
无封闭性
,不可再现
,相互制约
,结果不定
5. 多核的利弊
- 利:带来了CPU性能的提升,增强了用户体验
- 弊:核数过多,对并发程序的编写、调试等造成困难,造成
可编程性困扰
。
二. Go调度器
1. 基础知识:
- Golang并发是基于函数的,即让某个函数独立运行的能力。
- Golang并发基于
非抢占式多任务处理
,由协程主动交出控制权。 runtime.NumCPU()
是通过超线程技术
获取的本机逻辑CPU数量
。- Go1.5+默认的GOMAXPROCS为
runtime.NumCPU()
,适合cpu计算密集型
。
2. runtime调度器:
成员 | 说明 |
---|---|
GOARCH | CPU架构,如x386、amd64、arm等 |
GOOS | 操作系统架构,如darwin, linux等 |
GOROOT() | goroot目录 |
NumGoroutine() | 协程数(包含主协程) |
NumCPU() | 逻辑cpu数 |
GOMAXPROCS(n) | 设置获取硬件线程数 |
GC() | 垃圾回收 |
ReadMemStats(&info) | 统计内存 |
Gosched() | 暂停当前协程 |
Goexit() | 终止当前协程 |
- 示例1: 测试GOMAXPROCS
func sum(id int) {
var x int64
for i := 0; i < math.MaxUint32; i++ {
x += int64(i)
}
println(id, x)
}
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
for i := 0; i < 2; i++ {
go func(id int) {
defer wg.Done()
sum(id)
}(i)
}
wg.Wait()
}
GOMAXPROCS=2 time -p go run main.go
0 9223372030412324865
1 9223372030412324865
real 1.72 // 程序开始到结束时间差 (非 CPU 时间)
user 3.20 // ⽤用户态所使⽤用 CPU 时间片 (多核累加)
sys 0.09 // 内核态所使⽤用 CPU 时间片
调整GOMAXPROCS参数,观察real结果。
- 示例2: 直观感受调度器调度过程
// 模拟时间片
func main() {
//修改参数值,观察打印结果的离散度
runtime.GOMAXPROCS(4)
foo := func(n int) {
for {
fmt.Print(n)
}
}
go foo(0)
go foo(1)
time.Sleep(time.Second * 5)
fmt.Println("~~~Done~~~")
}
- 示例3: 退出子协程:
runtime.Goexit
将立即终止当前goroutine执行,调度器确保所有已注册defer延迟调用被执行。
func main() {
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
defer wg.Done()
defer println("A.defer")
func() {
defer println("B.defer")
runtime.Goexit()
println("B")
}()
println("A")
}()
wg.Wait()
}
- 示例4: 暂停子协程:
runtime.Gosched
让出底层线程,将当前goroutine暂停,放回队列等待下次被调度执行。
func main() {
wg := new(sync.WaitGroup)
wg.Add(2)
go func() {
defer wg.Done()
println("Hello, World!")
}()
func() {
defer wg.Done()
for i := 0; i < 6; i++ {
println(i)
if i == 3 {
runtime.Gosched()
}
}
}()
wg.Wait()
}
三. MPG模型
Go语言以 CSP(communicating sequential processes)
为并发模型
。不同于传统的多线程通过共享内存来通信
,CSP是**“以通信的方式来共享内存”**,
即 “不要以共享内存的方式来通信,相反,要通过通信来共享内存”
。
Do not communicate by sharing memory; instead, share memory by communicating.
Go语言使用MPG模式
来实现CSP,在传统的并发中起很多线程只会加大CPU和内存的开销,太多的线程会大量的消耗计算机硬件资源,造成并发量的瓶颈。
- M指的是
“Machine”
,一个M直接关联了一个内核线程。 - P指的是
”processor”
,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。 - G指的是
”Goroutine“
,其实本质上也是一种轻量级的线程(逻辑态线程
)。
M关联了一个内核线程,通过调度器P(上下文)的调度,可以连接1个或者多个G,相当于把一个内核线程切分成了了N个用户线程,M和P是一对一关系(但是实际调度中关系多
变),通过P调度N个G(P和G是一对多关系),实现内核线程和G的多对多关系(M:N),通过这个方式, 一个内核线程就可以起N个Goroutine,同样硬件配置的机器可用的用
户线程就成几何级增长,并发性大幅提高。
参考:
https://blog.csdn.net/erlib/article/details/50264271
https://studygolang.com/articles/10099
https://www.jianshu.com/p/8aa03db51043
https://www.jianshu.com/p/ff8e8be262ac
https://blog.haohtml.com/archives/18352
出让时间片或控制权轮转执行 ↩︎