线程、协程基本概念
协程是单线程下的并发,又称微线程,纤程。它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。英文名Coroutine。
一句话说明什么是协程?
轻量级的线程独立的栈空间,共享程序堆空间调度由用户控制是逻辑态,对资源消耗小。
线程和协程的区别
线程的切换是一个Cpu在不同线程中来回切换,是从系统层面来,不止保存和恢复CPU上下文这么简单,会非常耗费性能。但是协程只是在同一个线程内来回切换不同的函数,只是简单的操作CPU的上下文,所以耗费的性能会大大减少。
golang的协程机制,可轻松开启上万个协程。其他语言并发机制一般基于线程,开启过多资源耗费大。
func runTimes(nums int) {
for i := 0; i < nums; i++ {
fmt.Println("runTimes", i, "times")
time.Sleep(1000 * time.Millisecond)
}
}
func main() {
go runTimes(10)
for i := 0; i < 10; i++ {
fmt.Println("main", i, "times")
time.Sleep(2000 * time.Millisecond)
}
}
注意点:如果协程没有执行完,但是主线程已经结束,协程会直接结束。协程在主线程之前结束。那么协程的任务就完成了。
Go 语言 goroutine
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。
为此Go语言提供了 goroutine 这样的机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
使用 goroutine
有时 testRun() 并不执行,因为程序会为main函数创建一个默认的goroutine,当main里的语句执行完goroutine也就结束了,没有 go testRun() 执行的时间。为了确保go hello() 的goroutine能够执行可以延缓程序结束时间
func main() {
go testRun()
fmt.Println("main hello")
time.Sleep(time.Second)
}
func testRun() {
fmt.Println("hello")
}
单个 goroutine 可以通过时间延后来使这个 goroutine 被完全执行,但是当 goroutine 多到上百上千或更多时在使用 time.Sleep() 显然就没办法确定给多少时间来让 goroutine 被完全执行了,给多了影响程序效率,给少了有的 goroutine 又不会执行影响程序结果,这时候我们就要用到另一个东西那就是 sync.WaitGroup。
WaitGroup 对象内部有个计时器, 最初从0 开始, 他有3个方法 Add() , Done(), Wait() 用来控制计数器的数量。 Add(n) 把计数器设置成 n,Done() 每次把计数器 -1, wait() 会阻塞代码的运行, 直到计数器的值减为 0。将 goroutine 所剩数量与 WaitGroup 结合可以解决上述问题。
var wg sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go printNum(i)
}
wg.Wait()
fmt.Println("end")
}
func printNum(i int) {
defer wg.Done()
fmt.Println(i)
}
当我们运行这个代码就会发现每次的输出都不同,这是因为这 10 个 goroutine 的执行是并发的,而调度却是随机的。
goroutine 栈内存
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的 goroutine 也是可以的。
goroutine 调度
GPM 是Go语言运行时(runtime)层面的实现,是 go 语言自己实现的一套调度系统。区别于操作系统调度OS线程。
- G 就是个goroutine的,里面除了存放本 goroutine 信息外,还有与所在 P 的绑定等信息。
- P 管理着一组 goroutine 队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的 goroutine 队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
- M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一对一映射的关系, 一个groutine最终是要放到 M 上执行的。
在这里插入代码片
Go 语言 runtime 库
设置CPU最大核心数量
runtime.GOMAXPROCS(8)
查看当前CPU当前核心数量
fmt.Println(runtime.NumCPU())
runtime.Gosched()
退出当前的 goroutine ,为其他 goroutine 腾出执行空间,最后再执行被退出的 goroutine
大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤。
func main() {
// 让所有协程在一个核上执行
runtime.GOMAXPROCS(1)
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s,i)
}
}("协程运行中")
// 主协程
for i := 0; i < 2; i++ {
fmt.Println("hello")
// 停一下,再次分配任务
runtime.Gosched()
fmt.Println("world",i)
}
}
runtime.Goexit()
退出当前的 goroutine ,以后也不会执行。
一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了
runtime.GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函
数设置当前程序并发时占用的CPU逻辑核心数。
操作系统线程 vs goroutine关系
1. 一个操作系统线程对应用户态多个goroutine。
2. go程序可以同时使用多个操作系统线程。
3. goroutine和OS线程是多对多的关系,即m:n。
Go 语言 channel 管道
我们设置函数的意义就是为了在特定的输入下获取到特定的输出,如果只是让函数一味的并发而不进行值的传递,那么这个并发就是没有意义的。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
channel是个特殊的类型,通道类似传送带或是队列,遵循先进先出(First In First Out)的原则。
通道的声明和初始化
每个通道都是特定类型的,在声明时需要指明通道里传输的元素类型。
var 变量 chan 元素类型
var ch chan int
println(ch)
声明的通道后需要使用make函数初始化之后才能使用。初始化后的通道空值为一个十六进制的地址。
`在不进行初始化的情况下使用通道会报 deadlock`
`其中缓存大小是可选项`
make(chan 元素类型, [缓冲大小])
var ch1,ch2 chan int
ch1 = make(chan int) `无缓存的通道ch1`
ch2 = make(chan int,20) `缓存大小为20的通道ch2`
直接定义通道
ch3 := make(chan int)