Channel
共享内存
全局变量下内存是共享的,多线程共享内存来通信
- Go中原生的map不支持并发(若并发读写会报Fatal),但数组和切片以及
sync.Map是支持并发的,但是数组和切片的并发结果不可预计 - 需要加锁来访问共享内存
1 | package main |
并发下该代码的结果是不可预测的,可能是3个可能是2个也可能是1个
CSP
Communicating Sequential Processes,即以通信的方式来共享内存,在golang中就是goroutine负责并发执行,而channel负责在goroutine之间传递数据和信号
1 | package main |
生产者和消费者共享一个channel,生产者将数据传入channel,消费者从channel中取出数据并计算,实现共享内存
如实现合并多个小文件的功能,每个协程把读出来的部分内容放到channel中,再开一个协程取出channel中的内容并合并为一个大文件
死锁问题
Channel满了,阻塞写;Channel空了,阻塞读
阻塞后会交出CPU,执行其他协程,希望其他协程能帮自己解除阻塞
若阻塞在main协程中,没有其他子协程可以执行,则可以确定希望永远等不来,会自己把自己杀掉,报Fatal error
若阻塞发生在子协程中,就不会发生死锁,因为main协程给是一个值得等待的希望,会一直等待下去
并发
协程与线程
- 一对一:一个用户的线程对应一个内核线程
- m对n:多个用户线程和多个内核线程多对多对应
MPG
M:内核线程
P:虚拟处理器,代表M的上下文环境,是处理用户级代码逻辑的处理器,由环境变量中
GOMAXPROCS决定G:轻量级的线程,即goroutine
M要拿到P才能执行G
当一个goroutine阻塞时,M会释放绑定的P,将P转移到其他空闲的M上,继续执行G队列,若该goroutine解除阻塞,如果有空闲的P,就到对应的P上执行,否则进入全局可运行队列,P会周期扫描全局可运行队列,如果为空,则从其他P的等待队列里骰一半G过来
资源竞争
多协程并发修改同一块内存时,会产生资源竞争,可以在go run或go build执行时添加-race参数检查资源竞争情况
如:n++不是原子操作,分为取出n,+1,结果赋给n,开启并发后,可能会发生脏写,解决方法可以将整个操作变为原子操作,使用atomic.AddInt32
之类的方法可以解决资源竞争
更广泛的,可以使用读写锁:
var lock sync.RWMutex:声明读写锁
lock.Lock(), lock.Unlock():加/释放写锁
lock.RLock(), lock.RUnlock():加/释放读锁
任意时刻只能加一把写锁,且不能加读锁
若没加写锁时,可以同时加多把读锁,读锁加上后不能再加写锁
容器的并发安全性
- 数组、slice和struct允许并发修改,但可能会脏写
- 并发修改map会panic,如果并发修改需要使用
sync.Map,但是该map性能较低,若对性能要求高,可以用普通map并分段分给协程,若一定要访问同一段只能加锁 - 工程上一般解决方法是让多个协程处理数组不同部分的数据,避免脏写
sync.Once()
在高并发场景下确保有些事情只执行一次,比如加载配置文件、关闭管道等,创建once后在对应的do函数内创建方法即可
1 | type Singleton struct {} |
切片
切片的底层是结构体,底层是一个结构体:
1 | type slice struct { |
- array是一个指针,指向一个底层数组的首
- len表示数组的长度
- cap表示数组的预留空间,最多容纳几个元素,多出来的部分是读不出来的
- append追加的元素放到预留的空间,并len+1,如果预留空间已经用完,则会重新申请一块更大的内存空间
1 | arr := make([]int, 3, 5) |
结果是:
它们共享底层数组,指向同一块内存空间
- 若预留空间用完,会重新申请一块更大的内存空间,这样会把原内存空间的数据拷贝过来,因此性能会变慢
母子切片内存共享
1 | s := make([]int, 3, 5) |
- 刚开始子切片和母切片共享底层内存空间,修改子切片会反应到母切片中,在子切片上执行append会把新元素放到母切片预留的空间上
1 | arr := make([]int, 3, 5) |
- 因为它们共享内存空间,因此无论是将crr的1号索引值修改为8,还是crrappend一个4,都会对arr造成影响
- 若不断append,当超出arr的cap,crr就和arr的内存分离了,因为申请了新的内存空间,这时两者互不相干
内存泄漏
认为GC会回收的不用的内存并没有被回收,导致内存泄漏
1 | func return_sub_s() []int { |
若parent持有一个很大的数组,占用大量内存,但子切片很小,这时如果子切片一直在使用,parent就得不到释放,函数调用多次导致内存使用增加
函数参数需要用切片指针吗
假设传入的数组cap=3
1 | func changeSlice(arr *[]int) { |
- 第一个追加元素,len和cap都要变(cap=6),所以要传切片指针
- 第二个截取元素,len、pointer和cap都变(cap=5),需要传切片指针
- 第三个修改切片元素,只改变底层数组,不需要传切片指针
遍历修改切片
Golang中for range取得的是集合中元素的拷贝
1 | for _, ele := range arr { |
第二种方式才能修改成功