Toasobi
Golang并发编程笔记
- 同步实现
基础暴力就是用time.sleep()了,更好的办法是用sync.WaitGroup来实现goroutine的同步
- 可增长栈
os线程通常有固定的栈内存,一个goroutine的栈在其生命周期开始时只有很小的内存(2kb),但是往后其内存不固定,会根据情况自动调整大小,最大可达到1g。一般情况一个go线程用内存很小,所以即是开十万个goroutine也没有啥问题
- goroutine调度系统GPM
go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
- runtime包
关于runtime.Gosched,其实就是让出cpu,好使其他线程有执行计划,最后的结果和syncGroup差不多。
其中还有退出协程,更改调度器使用的os线程(使用cpu几个逻辑核心数)
- 操作系统线程和goroutine的关系
1.一个操作系统线程对应用户态多个goroutine
2.go程序可以同时使用多个操作系统线程
3.goroutine和os线程是多对多关系 m:n
- channel
熟悉channel类型,(var ch1 chan int // 声明一个传递整型的通道)
创建channel(声明如上,创建用make),
channel操作,发送,接收,关闭(不能发但能收)
无缓冲:无缓冲的通道只有在有人接收值的时候才能发送值,必须启动协程接收,即送货上门。
有缓冲:有快递柜,我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
优雅的循环取值,即知道从管道拿取数据时会有bool返回值,要判断。同时打印可以使用for range
单向通道:即在参数的chan前后加箭头
- goroutine池
生产者消费者模型嘛,这个熟
要开几个协程,就多少次循环开就完了
- 定时器
timer
package main
import (
"fmt"
"time"
)
func main() {
// 1.timer基本使用
//timer1 := time.NewTimer(2 * time.Second)
//t1 := time.Now()
//fmt.Printf("t1:%v\n", t1)
//t2 := <-timer1.C
//fmt.Printf("t2:%v\n", t2)
// 2.验证timer只能响应1次
//timer2 := time.NewTimer(time.Second)
//for {
// <-timer2.C
// fmt.Println("时间到")
//}
// 3.timer实现延时的功能
//(1)
//time.Sleep(time.Second)
//(2)
//timer3 := time.NewTimer(2 * time.Second)
//<-timer3.C
//fmt.Println("2秒到")
//(3)
//<-time.After(2*time.Second)
//fmt.Println("2秒到")
// 4.停止定时器
//timer4 := time.NewTimer(2 * time.Second)
//go func() {
// <-timer4.C
// fmt.Println("定时器执行了")
//}()
//b := timer4.Stop()
//if b {
// fmt.Println("timer4已经关闭")
//}
// 5.重置定时器
timer5 := time.NewTimer(3 * time.Second)
timer5.Reset(1 * time.Second)
fmt.Println(time.Now())
fmt.Println(<-timer5.C)
for {
}
}
ticker
package main
import (
"fmt"
"time"
)
func main() {
// 1.获取ticker对象
ticker := time.NewTicker(1 * time.Second)
i := 0
// 子协程
go func() {
for {
//<-ticker.C
i++
fmt.Println(<-ticker.C)
if i == 5 {
//停止
ticker.Stop()
}
}
}()
for {
}
}
- select
监听多个channel,如果多个channel同时ready,则随机挑选一个执行,同时它的default还可以用来判断管道是否满了
- 并发安全和锁
十字路口设置红绿灯嘛,知道互斥锁和读写互斥锁就行
- sync
除了sync.WaitGroup之外,还有一些
sync.Once
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
它只有一个do方法,下面是用法
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons) //如果是普通调用则遇到多协程调用的时候会有并发安全问题
return icons[name]
}
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
sync.Map
Go语言中内置的map不是并发安全的,当设置键值和通过键值拿取数据被分装为函数并被多个协程调用时,就会有并发安全问题。
像这种场景下就需要为map加锁来保证并发的安全性了
开箱即用:
var m = sync.Map{}
同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
- 原子操作
针对基础数据类型更有效率的加锁操作,打破锁的内核切换,实现用户态之间的切换,性能优异
atomic包,里面有读取,写入,修改,交换,比较并交换的操作方法