目录
1.基础知识
并发与并行
(从线程与CPU的关系角度分析)
并发:多个线程在单核上运行,宏观上同时运行,微观上某一时刻只有一个线程在运行
并行:多线程在多核上运行,无论宏观上还是微观上,某一时刻有多个线程在同时运行
进程和线程
进程:程序在操作系统中一次执行过程,系统进行资源分配和调度的基本单位
线程:进程的一个执行实例,是程序执行的最小单元,比进程更小的能独立运行的基本单位
一个进程创建和销毁多个线程,同一个进程中的线程可以并发执行
go协程(goroutine)与go主线程
一个Go主线程(线程或进程)可以起多个协程;
主线程是物理线程,直接对CPU资源划分,耗费CPU;
协程是主线程开启的轻量级线程,属于逻辑态,资源消耗小,不直接作用CPU,可以轻松开启上万个协程;
-
协程的数量与运行效率提升也不是完全成正比,因为CPU资源与运算速率也是有限的,当CPU跑满之后,开启再多的线程也无法提升效率。这也是golang的一种并发优势。其他编程语言一般是基于线程的。
-
运行时主线程与协程同时执行(多核情况下并行,单核情况下,并发) 主线程退出了,即使协程还没有执行完毕,也会退出
go协程特点
- 协程–轻量级的线程
- 调度由用户控制:
用户可以控制协程之间的调度顺序(关系),而线程之间的调度关系是由操作系统控制的,用户无法操控 - 有独立的栈空间
- 共享程序堆空间
goroutine退出机制
主线程退出,协程退出
协程执行完毕后自己退出
goroutine的MPG(执行模式
G: Goroutine,即我们在 Go 程序中使用 go 关键字创建的执行体;
M: Machine,或 worker thread,即传统意义上进程的线程;
P: Processor,即一种人为抽象的、用于执行 Go 代码被要求局部资源。只有当 M 与一个 P 关联后才能执行 Go 代码。除非 M 发生阻塞或在进行系统调用时间过长时,没有与之关联的 P。
https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/mpg/
当一个协程G0在主线程M0上出现阻塞时,会重新创建一个新的主线程M1(或从已有的线程池中取出一个线程M1)执行后面的协程G1,这种调度模式,可以让G0执行,同时避免了后面的协程阻塞,实现并发/并行。
goroutine运行范例
package main
import (
"fmt"
"strconv"
"time"
)
// 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
// 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
// 要求主线程和goroutine同时执行
//编写一个函数,每隔1秒输出 "hello,world"
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("tesst () hello,world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println(" main() hello,golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
// output
main() hello,golang1
tesst () hello,world 1
tesst () hello,world 2
main() hello,golang2
tesst () hello,world 3
main() hello,golang3
main() hello,golang4
tesst () hello,world 4
tesst () hello,world 5
tesst () hello,world 8
tesst () hello,world 9
main() hello,golang9
main() hello,golang10
tesst () hello,world 10
设置CPU个数
package main
import (
"fmt"
"runtime"
)
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum)
//可以自己设置使用多个cpu
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok")
}
2. 实例:goroutine实现 1-50 的各个数的阶乘的计算
需求:现在要计算 1-50 的各个数的阶乘,并且把各个数的阶乘放入到map中, 最后显示出来。要求使用goroutine完成
思路:
- 编写一个函数,来计算各个数的阶乘,并放入到 map中.
- 我们启动的协程多个,统计的将结果放入到 map中
- map 应该做出一个全局的.
package main
import (
"fmt"
"sync"
)
var (
myMap = make(map[int]int, 10)
)
// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我们将 res 放入到myMap
myMap[n] = res //出现错误 concurrent map writes? 多个协程对map空间并发写入,
}
func main() {
// 开启多个协程完成任务[50个]
for i := 1; i <= 50; i++ {
go test(i)
}
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
不会有任何输出
原因:协程未执行完毕时,主线程已经退出,所以没有任何输出
解决方案1:加锁,如下所示
// 方案1:加锁
package main
import (
"fmt"
"sync"
"time"
)
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁,
//sync 是包: synchornized 同步
Mutex : 是互斥
lock sync.Mutex
)
// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我们将 res 放入到myMap
//加写锁
lock.Lock()
myMap[n] = res //concurrent map writes?
解锁
lock.Unlock()
}
func main() {
// 开启多个协程完成任务[50个]
for i := 1; i <= 50; i++ {
go test(i)
}
//休眠5秒钟
time.Sleep(time.Second * 5)
//加读锁;为什么?
lock.Lock()
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}
如果不加写锁,将不会有有任何输出,协程没有执行完毕,主线程就执行结束,所以协程也跟着结束
func main 不加读,运行 go build -race main.go
结果出现数据竞争 :Found 2 data race(s) (示例如下)
原因分析:main函数读取部分为什么需要加互斥锁,按理说5秒数上面的协程都应该执行完,后面就不应该出现资源竞争的问题了,但是在实际运行中,还是可能在红框部分出现(运行时增加-race参数,确实会发现有资源竞争问题),因为我们程序从设计上可以知道5秒就执行完所有协程,但是主线程并不知道,因此底层可能仍然不断尝试读取,从而出现资源争夺,因此加入互斥锁即可解决问题
不添加读锁,仍然会有数据竞争
//不添加读锁,仍然会有数据竞争
map[27]=-5483646897237262336
map[29]=-7055958792655077376
map[36]=9003737871877668864
map[48]=-5844053835210817536
map[24]=-7835185981329244160
map[18]=6402373705728000
map[21]=-4249290049419214848
map[22]=-1250660718674968576
map[25]=7034535277573963776
map[38]=4789013295250014208
map[42]=7538058755741581312
map[49]=8789267254022766592
map[14]=87178291200
map[50]=-3258495067890909184
map[15]=1307674368000
map[19]=121645100408832000
map[26]=-1569523520172457984
map[28]=-5968160532966932480
map[41]=-2894979756195840000
map[43]=-7904866829883932672
map[44]=2673996885588443136
map[2]=2
map[46]=1150331055211806720
map[11]=39916800
map[16]=20922789888000
map[33]=3400198294675128320
map[34]=4926277576697053184
map[7]=5040
map[13]=6227020800
map[23]=8128291617894825984
map[30]=-8764578968847253504
map[35]=6399018521010896896
map[47]=-1274672626173739008
map[8]=40320
map[6]=720
map[10]=3628800
map[3]=6
map[4]=24
map[5]=120
map[20]=2432902008176640000
map[31]=4999213071378415616
map[32]=-6045878379276664832
map[37]=1096907932701818880
map[39]=2304077777655037952
map[1]=1
map[45]=-8797348664486920192
map[40]=-70609262346240000
map[12]=479001600
map[17]=355687428096000
map[9]=362880
Found 2 data race(s)
exit status 66
同时添加读写锁,无竞争冲突
//同时添加读写锁,无竞争冲突
map[8]=40320
map[21]=-4249290049419214848
map[17]=355687428096000
map[13]=6227020800
map[39]=2304077777655037952
map[5]=120
map[25]=7034535277573963776
map[38]=4789013295250014208
map[29]=-7055958792655077376
map[40]=-70609262346240000
map[44]=2673996885588443136
map[3]=6
map[7]=5040
map[24]=-7835185981329244160
map[26]=-1569523520172457984
map[35]=6399018521010896896
map[47]=-1274672626173739008
map[33]=3400198294675128320
map[27]=-5483646897237262336
map[1]=1
map[2]=2
map[10]=3628800
map[15]=1307674368000
map[20]=2432902008176640000
map[19]=121645100408832000
map[42]=7538058755741581312
map[46]=1150331055211806720
map[4]=24
map[16]=20922789888000
map[14]=87178291200
map[32]=-6045878379276664832
map[31]=4999213071378415616
map[28]=-5968160532966932480
map[36]=9003737871877668864
map[34]=4926277576697053184
map[9]=362880
map[11]=39916800
map[12]=479001600
map[22]=-1250660718674968576
map[23]=8128291617894825984
map[41]=-2894979756195840000
map[6]=720
map[18]=6402373705728000
map[30]=-8764578968847253504
map[43]=-7904866829883932672
map[45]=-8797348664486920192
//没有数据竞争,但存在数据溢出
加锁的主要目的是为了解决协程与主线程的通讯问题,那么,如何让主程序自动判定协程是否结束?channel是更好的解决方案。
参考: 【尚硅谷】Golang入门到实战教程丨一套精通GO语言 P264–P271