第八章 Go并发
Go语言区别于其他语言的一大特点就是出色的并发性能,最重要的一个特性那就是go
关键字。
并发场景:
- UI小姐姐一边开着PS软件,一边微信疯狂的和产品经理打字交流,后台还听着网易云音乐。。
- 双11当天。。大伙疯狂的访问淘宝网站
- CPU从单核向多核发展,计算机程序不该是串行的,浪费资源
- 串行程序由于IO操作被阻塞,整个程序处于停滞状态,其他IO无关的任务无法执行
并发必要性:
- 充分利用CPU核心的优势,提高程序执行效率
实现并发的模型:
- 多进程,多进程是在操作系统层面并发的基本模式,进程间互不影响,但是开销最大,进程由内核管理。
- 多线程,属于系统层面的并发模式,也是用的最多的有效模式,大多数软件使用多线程,开销小于多进程。
- 基于回调的非阻塞/异步IO。此架构处于多线程模式的危机,高并发服务器下,多线程会消耗殆尽服务器的内存和CPU资源。而通过事件驱动的方式使用异步IO,尽可能少用线程,降低开销,Node.js就是如此实践,但是此模式编程复杂度较高。
- 协程,Coroutine是一种用户态线程,寄存于线程中,系统开销极小,可以有效提高线程任务并发性,使用方式简单,结构清晰,避免多线程的缺点。需要编程语言的支持,如不支持,需要用户自行实现调度器。
共享内存系统
是比较常用的并发模式,线程之间通信采用共享内存的方式,程序员需要加锁等操作避免死锁、资源竞争等问题。
计算机科学家又研制出了消息传递系统
,对线程间共享状态的各种操作被封装在线程之间传递的消息中
。
Communicating Sequential Processes(顺序通信进程),在CSP系统中,所有的并发操作都是通过独立线程以异步的方式运行,这些线程必须通过再彼此之间发送消息,从而向另一个线程请求信息。
1.1. 进程和线程
进程
是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。
线程
是进程的一个执行实例,是比进程更小的独立运行的基本单位。
进程可以创建或销毁多个线程,同一个进程中的多个线程可以并发执行(如百度云盘进程中的,多个下载任务)。
一个程序至少一个进程,一个进程至少一个线程。
“并发”指的是程序的结构,“并行”指的是程序运行时的状态
1.1.1. 并行(parallelism)
这个概念很好理解。所谓并行,就是同时执行的意思,无需过度解读。判断程序是否处于并行的状态,就看同一时刻是否有超过一个“工作单位”在运行就好了。
所以,单线程永远无法达到并行状态。
要达到并行状态,最简单的就是利用多线程和多进程。
但是 Python 的多线程由于存在著名的 GIL,无法让两个线程真正“同时运行”,所以实际上是无法到达并行状态的。
1.1.2. 并发(concurrency)
要理解“并发”这个概念,必须得清楚,并发指的是程序的“结构”。
当我们说这个程序是并发的,实际上,这句话应当表述成“这个程序采用了支持并发的设计”。好,既然并发指的是人为设计的结构,那么怎样的程序结构才叫做支持并发的设计?
正确的并发设计的标准是:使多个操作可以在重叠的时间段内进行(two tasks can start, run, and complete in overlapping time periods)。
1.1.3. 并发和并行
1)多线程程序在单核CPU上运行,就是并发
2)多线程程序在多核CPU上运行,就是并行
为何人们常说提升并发
,而不是提升并行
?
因为并发
是通过时间片轮转
进行进程调度,是通过技术手段提升并发。
而并行
是通过硬件提升效率,有钱人可以买一个128核的服务器。
1.2. 协程是什么
执行单位是个抽象的概念,操作系统层面有多个概念与之对应,比如操作系统掌管的进程(process)、进程内的线程(thread)以及进程内的协程(coroutine)
。
协程在于轻量级
,轻松创建百万个
而不会导致系统资源衰竭。
多数语言语法层面不直接支持协程,而是通过库的方式支持,然而库的功能也仅仅是线程的创建、销毁与切换,而无法达到协程调用同一个IO操作,如网络通信,文件读写等。
非抢占式多任务处理,由协程主动交出控制权。消耗的资源更少
编译器/解释器/虚拟机层面的多任务,实现的协程调度。
多个协程可能在一个或多个线程上运行。
1.3. 其他语言的协程
c++ Boost.Coroutine Java不支持 Python 用yield关键字实现协程 3.5之后async def对协程支持,异步函数的定义 golang 不需要定义时区分是否是异步函数 go func(){}
1.4. goroutine
Golang在语言层面支持协程,名为goroutine
,Go语言标准库提供所有系统调用操作,都会让出CPU给其他goroutine,使得协程切换管理不依赖于系统的线程和进程,也不依赖于CPU核数。
一个Go进程,可以启动多个goroutine协程。
main函数也是goroutine。
一个普通的机器运行几十个线程负载已经很高了,然而可以轻松创建百万个
goroutine。
go标准库的net包,写出的go web server性能直接媲美Nginx。
1.5. goroutine可能的切换点
I/O select channel 函数调用 runtime.Gosched() 等待锁
1.6. goroutine入门
第一个goroutine,开启协程,执行函数hello()
package main import ( "fmt" "time" ) func hello() { fmt.Println("hello goroutine") } func main() { go hello() fmt.Println("main thread terminate") time.Sleep(time.Second) }
批量开启协程
package main import ( "fmt" "time" ) func hello(i int) { fmt.Println("hello goroutine", i) } func main() { //循环开启10个协程,分别执行hello()函数 for i := 0; i < 10; i++ { go hello(i) } time.Sleep(time.Second) }
编写代码,完成功能
1.在go主进程中,开启goroutine,该协程每秒
输出一个你好,我是goroutine
2.在主进程中也每秒
输出一个我很好,我是主进程
,输出10次后退出程序
3.要求主进程和goroutine同时执行
package main import ( "fmt" "strconv" "time" ) //定义一个协程任务函数 func test() { for i := 0; i <= 10; i++ { fmt.Println("你好,我是goroutine" + strconv.Itoa(i)) time.Sleep(time.Second) //睡眠1秒 } } func main() { go test() for i := 0; i <= 10; i++ { fmt.Println("我很好,我是主进程" + strconv.Itoa(i)) time.Sleep(time.Second) //睡眠1秒 } }
提示:检测主进程结束,协程也立即结束,或是检测主进程未结束,协程提前退出,可以修改for循环的次数!
1.7. runtime包控制goroutine
runtime.Gosched()让出时间片,如同接力赛跑,让出了接力棒。
gosched如同yield作用,暂停当前的goroutine,放回队列等待下次执行。
package main import ( "fmt" "runtime" ) func main() { go func() { for i := 0; i < 5; i++ { fmt.Println("你愁啥") } }() for i := 0; i < 2; i++ { //让出时间片,让其他协程执行 runtime.Gosched() fmt.Println("尼古拉斯赵四") } }
runtime.Goexit()终止当前协程
package main import ( "fmt" "runtime" "time" ) func test() { defer fmt.Println("ccc") //return //函数终止,打印a c b 结束 runtime.Goexit() //退出所在协程, 打印 a c 退出主进程 fmt.Println("ddd") } func main() { go func() { fmt.Println("aaa") test() fmt.Println("bbb") }() // time.Sleep(time.Second * 3) }
Go与多核的优势,设置cpu运行数目
go version < 1.8 需要手动设置多核
go version > 1.8 默认用多核,无须设置
package main import ( "fmt" "runtime" ) func main() { cpuNum := runtime.NumCPU() //可以在这演示下单核时,时间片无切换,仅仅打印数字0的实验 runtime.GOMAXPROCS(1) runtime.GOMAXPROCS(cpuNum) for i := 0; i < 500; i++ { go fmt.Print(1) fmt.Print(0) } }
1.8. goroutine使用recover
package main import ( "fmt" "time" ) func test() { defer func() { if err := recover(); err != nil { fmt.Println("出错了:", err) } }() var m map[string]int //map必须初始化才能使用 //m=make(map[string]int,10) m["stu"] = 111 } func calc() { for { fmt.Println("我是calc函数") time.Sleep(time.Second) } } func main() { go test() //协程执行函数,这个函数有错的话,程序会panic退出,做好异常捕捉 for i := 0; i < 2; i++ { go calc() } time.Sleep(time.Second * 10) }
来源:https://www.cnblogs.com/open-yang/p/11256899.html