目录
- 1.基础知识
- 并发与并行
- 进程和线程
- go协程(goroutine)与go主线程
- go协程特点
- goroutine退出机制
- goroutine的MPG(执行模式
- goroutine运行范例
- 设置CPU个数
- 2. 实例:goroutine实现 1-50 的各个数的阶乘的计算
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 mainimport ( "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,golang1tesst () hello,world 1tesst () hello,world 2 main() hello,golang2tesst () hello,world 3 main() hello,golang3 main() hello,golang4tesst () hello,world 4tesst () hello,world 5tesst () hello,world 8tesst () hello,world 9 main() hello,golang9 main() hello,golang10tesst () hello,world 10
设置CPU个数
package mainimport ( "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 mainimport ( "fmt" "sync")var ( myMap = make(map[int]int, 10))// test 函数就是计算 n!, 让将这个结果放入到 myMapfunc 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]=%dn", i, v) }}
不会有任何输出
原因:协程未执行完毕时,主线程已经退出,所以没有任何输出
解决方案1:加锁,如下所示
// 方案1:加锁package mainimport ( "fmt" "sync" "time")var ( myMap = make(map[int]int, 10) //声明一个全局的互斥锁 //lock 是一个全局的互斥锁, //sync 是包: synchornized 同步 Mutex : 是互斥 lock sync.Mutex)// test 函数就是计算 n!, 让将这个结果放入到 myMapfunc 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]=%dn", i, v) } lock.Unlock()}
如果不加写锁,将不会有有任何输出,协程没有执行完毕,主线程就执行结束,所以协程也跟着结束
func main 不加读,运行 go build -race main.go
结果出现数据竞争 :Found 2 data race(s) (示例如下)
原因分析:main函数读取部分为什么需要加互斥锁,按理说5秒数上面的协程都应该执行完,后面就不应该出现资源竞争的问题了,但是在实际运行中,还是可能在红框部分出现(运行时增加-race参数,确实会发现有资源竞争问题),因为我们程序从设计上可以知道5秒就执行完所有协程,但是主线程并不知道,因此底层可能仍然不断尝试读取,从而出现资源争夺,因此加入互斥锁即可解决问题
不添加读锁,仍然会有数据竞争
//不添加读锁,仍然会有数据竞争map[27]=-5483646897237262336map[29]=-7055958792655077376map[36]=9003737871877668864map[48]=-5844053835210817536map[24]=-7835185981329244160map[18]=6402373705728000map[21]=-4249290049419214848map[22]=-1250660718674968576map[25]=7034535277573963776map[38]=4789013295250014208map[42]=7538058755741581312map[49]=8789267254022766592map[14]=87178291200map[50]=-3258495067890909184map[15]=1307674368000map[19]=121645100408832000map[26]=-1569523520172457984map[28]=-5968160532966932480map[41]=-2894979756195840000map[43]=-7904866829883932672map[44]=2673996885588443136map[2]=2map[46]=1150331055211806720map[11]=39916800map[16]=20922789888000map[33]=3400198294675128320map[34]=4926277576697053184map[7]=5040map[13]=6227020800map[23]=8128291617894825984map[30]=-8764578968847253504map[35]=6399018521010896896map[47]=-1274672626173739008map[8]=40320map[6]=720map[10]=3628800map[3]=6map[4]=24map[5]=120map[20]=2432902008176640000map[31]=4999213071378415616map[32]=-6045878379276664832map[37]=1096907932701818880map[39]=2304077777655037952map[1]=1map[45]=-8797348664486920192map[40]=-70609262346240000map[12]=479001600map[17]=355687428096000map[9]=362880Found 2 data race(s)exit status 66
同时添加读写锁,无竞争冲突
//同时添加读写锁,无竞争冲突map[8]=40320map[21]=-4249290049419214848map[17]=355687428096000 map[13]=6227020800 map[39]=2304077777655037952 map[5]=120 map[25]=7034535277573963776 map[38]=4789013295250014208 map[29]=-7055958792655077376map[40]=-70609262346240000 map[44]=2673996885588443136 map[3]=6 map[7]=5040 map[24]=-7835185981329244160map[26]=-1569523520172457984map[35]=6399018521010896896 map[47]=-1274672626173739008map[33]=3400198294675128320 map[27]=-5483646897237262336map[1]=1 map[2]=2 map[10]=3628800 map[15]=1307674368000 map[20]=2432902008176640000 map[19]=121645100408832000 map[42]=7538058755741581312map[46]=1150331055211806720map[4]=24map[16]=20922789888000map[14]=87178291200map[32]=-6045878379276664832map[31]=4999213071378415616map[28]=-5968160532966932480map[36]=9003737871877668864map[34]=4926277576697053184map[9]=362880map[11]=39916800map[12]=479001600map[22]=-1250660718674968576map[23]=8128291617894825984map[41]=-2894979756195840000map[6]=720map[18]=6402373705728000map[30]=-8764578968847253504map[43]=-7904866829883932672map[45]=-8797348664486920192//没有数据竞争,但存在数据溢出
加锁的主要目的是为了解决协程与主线程的通讯问题,那么,如何让主程序自动判定协程是否结束?channel是更好的解决方案。
参考: 【尚硅谷】Golang入门到实战教程丨一套精通GO语言 P264–P271