golang 中 make 和 new 的区别?
-
共同点:
-
给变量分配堆内存
-
-
不同点:
-
作用变量类型不同,make仅能给切片,map,channel分配内存,new分不限制;
-
返回类型不一样,new返回指向变量的指针,make返回变量本身;
-
new分配内存,make分配+初始化
-
-
补充:堆内存和栈内存
-
stack用于静态分配内存,空间连续;heap动态分配,空间无序
-
stack内存由编译器自动执行;heap内存程序员手动分配
-
访问stack内存快;访问heap慢
-
stack内存主要问题为内存不足;heap为易产生内存泄漏
-
数组和切片的区别
-
定长/不定长,数组是值类型,切片是引用类型,切片本身是一个包含指向slice中第一个元素指针、len、cap的结构体
-
补充1:什么是值类型和引用类型?
-
值类型:使用这些类型的变量直接指向存在内存中的值,数组和结构体是值类型
-
引用类型:变量存放的是一个内存地址值,这个地址值指向的空间存的才是最终的值,内存通常在heap中分配
-
-
补充2:数组和切片都是值传递,所以可以在函数中修改切片的值,这样相当于传入指针,而不能append,因为传入的len和cap是值,append的话要设置函数返回值
-
补充3:切片扩容,容量小于1024,两倍扩容,否则1.25倍扩容
map
type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // 有2的B次方个桶 hash0 uint32 // hash seed noverflow uint16 // 溢出桶的数量 buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // 迁移新桶时记录的下一个要被迁移的旧桶编号 extra *mapextra // 里面会记录一些溢出桶的信息(当一个桶满了,hash运算还往桶里装数据时会放进溢出桶) }
type bmap struct { // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. tophash [bucketCnt]uint8 //哈希值相同(低位相同) 高位用来区别桶中多个kv对 // Followed by bucketCnt keys and then bucketCnt elems. // NOTE: packing all the keys together and then all the elems together makes the // code a bit more complicated than alternating key/elem/key/elem/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer. }//桶中的键值对以kkkvvv存放
map并发报错 多协程并发读写map会报错
解决方法:1.封装一个带读写锁RWMutex的结构体 2.用sync.Map
map gc回收机制 map只增不减的数组结构
delete只是逻辑上delete,其实是被打标记,但不会被gc回收,赋值nil可以使被回收(这样做是因为 防止后续会有相同的 key 插入,省去了扩缩容的操作)
map循环为什么是无序的? map扩张时会重新哈希,各键值对的存储位置会发生变化;for range遍历时顺序随机
map底层结构 实现原理是hash表,map在golang中是hmap结构体,包含若干个bucket,每个bucket都是指针
查找流程:计算哈希值,根据哈希值低位去找到bucket,将高位与现有的tophash数组中key匹配,找不到就到下个溢出的桶中查找
map扩容
-
触发扩容条件:存储数量/桶的数量>6.5,*2扩容,新插入的kv就会存放到新bucket中,逐步搬迁,每次查询搬两个kv对 ldbuckets:扩容前桶 buckets:扩容后的桶,搬迁后(old里的所有搬来后,放在第一个1,新加的放到后面的1)会删除old
-
桶满但没触发扩容(溢出桶) 链上溢出桶(溢出桶:内存上与常规桶是连续的,当B>4时,会预先预留2的B-4次方个溢出桶备用) Golang底层原理剖析之map_cheems~的博客-CSDN博客_golang map底层
map的key类型 可比较的类型都可作为key,不可比较的是slice、map、function
channel
本身适用场景:协程通信方式,本身自带锁,保证读写安全,天生支持并发 分为有无缓冲的、单双向的,三种状态为关未初始化 正常 关闭 golang 系列:channel 全面解析 - 知乎
type hchan struct { qcount uint // channel 里的元素计数 dataqsiz uint // 可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz elemsize uint16 // 要发送或接收的数据类型大小 buf unsafe.Pointer // 当 channel 设置了缓冲数量时,该 buf 指向一个存储缓冲数据的区域,该区域是一个循环队列的数据结构 closed uint32 // 关闭状态 sendx uint // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已发送数据的索引位置 recvx uint // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已接收数据的索引位置 recvq waitq // 想读取数据但又被阻塞住的 goroutine 队列 sendq waitq // 想发送数据但又被阻塞住的 goroutine 队列 lock mutex ... }
发送流程 1. 对于有缓冲chan 缓冲区没满:加入缓冲区尾 缓冲区满:加入写等待队列 2. 对于无缓冲chan 有读等待队列:数据发送到读等待队列队首,唤醒协程 无读等待队列:发送数据的协程加入到写等待队列对尾,挂起 接收流程 1. 对于有缓冲chan 缓冲区空:加入读等待队列 缓冲区有数据:读取队头 缓冲区满且有写等待队列:读取缓冲区队头,把写等待队列第一个数加入缓冲区队尾,唤醒 2. 对于无缓冲chan 有写等待队列:直接获取写等待队列队首数据,唤醒协程 无写等待队列:加入读等待队列
判断是否已经关闭
_,ok:=<-chan1,此时关闭的chan返回的ok为false
只能在发送端关闭channel原因:接收方可以通过以上方式感知到channel关闭,发送方感知不到
chanel死锁
range会直到channel被close才会停止,停止前会阻塞后面的语句,需要close的场景,使用range遍历channel时(有缓冲区的)。发送者没有关闭 channel 或在 range 之后关闭,都会导致 deadlock(死锁)。
for v := range chans { fmt.Println(v) }
chanel补充
单向channel应用场景:函数参数,从而达到函数内只能对channel发送 或只能接收,其余时候几乎不用到 只读channel(<-chan int)不能close,原因是只读channel只有读权限没有写权限,所以无法关闭;只写chan可以close
阻塞场景:写满或者读空
不带buffer的channel:用于同步通信。 带buffer的channel:用于异步通信。
channel超时处理:case <-time.After
select(多路IO复用机制)
-
规则
-
监听多个channel
-
case只能处理channel类型通信操作
-
多个满足条件case会随机执行一个,有default执行default,没满足的case也没default则阻塞
-
读操作要判断是否成功读取,关闭的channel也可以读取
-
-
机制: 1. 先锁定所有chan 2. 随机检查是否满足case,又满足就读写chan,解锁其它chan,返回;否则default,都无则将当前协程阻塞加入等待队列 3. 等待队列中的协程若被唤醒,就解锁其它chan,返回
type scase struct { c *hchan // chan,一个case只能监听一个chan kind uint16 // chan是可读可写还是default elem unsafe.Pointer // data element缓冲区,表示将要读出/写入channel的数据存放地址 }
例子:for { select { case c = <-ch: fmt.Println("Receive C", c) case s := <-ch: fmt.Println("Receive S", s) case <-time.After(5 * time.Second): _ = <-stopCh fmt.Println("END") goto end } }
用处:通知子协程退出,其它多通道的读写(_,ok:=<-ch if!ok{})
defer
-
功能:释放资源、收尾,如释放锁,关闭文件,异常处理
-
执行顺序:后进先出
-
实现:每个defer都是一个实例,多个实例用指针连接形成链表,每次在表头插入,从表头取出
-
注意:return后的defer不会执行 defer函数入参在语句出现时就已确定 可以操作主函数返回值 defer中所用到的参数会实时解析,所以如果外层是for循环需要注意
匿名返回值和命名返回值对defer的影响 匿名返回值:函数返回时会自动创建一个变量ret,return res时把res赋值给ret,defer中再对res++之影响res,不影响ret 命名返回值:函数定义时返回值变量已定义为res,所以之后的defer中res++是直接操作的返回值
GMP模型
G:goroutine M: thread,内核级线程,所有的G都要放在M上才能运行 P: processor,调度G到M上,其维护了一个队列,存储了所有需要它来调度的G
结构组成 全局队列 P维护的本地队列 P M 调度流程 1.新建G 2.优先放到本地队列中,满了把本地队列一半放全局队列 3.M执行时优先从绑定的P队列获取G,如果P为空,就从其他P的队列偷一半G放到自己队列 或 从全局队列拿一部分到自己队列 4.调度G到M上 5.M执行G,若阻塞,则释放P,把P转到其它空闲M或新建M上执行 6.执行完销毁G,再次从P队列中获取G
补充1:图可见浅谈go语言中GMP调度原理 - 知乎
补充2:
进程:资源分配(内核态),有自己独立的内存空间 线程:CPU调度单位(内核态),从属进程,可通过共享进程资源进行线程间通信 协程:由程序控制(用户态),不受操作系统调度,按调度策略把协程调度到线程中执行
goroutine调度策略
goroutine调度是用户态的,上下文切换对性能影响
时机:系统调用、程序阻塞(锁或通道)、手动调用runtime.Gosched
1.基于协作的抢占式调度 一个goroutine最多占cpu10ms,防止其他goroutine饿死,但切换到下一个协程的前提是当前协程主动call 但也有一些情况无法让出资源:死循环for 2.信号量抢占式调度(异步抢占):最新 sysmon 线程检测到执行时间过长的goroutine时,会向M发送一个信号,M上有该信号处理函数,可将当前goroutine插到全局队列,切换下一个G
补充:常见的调度策略有协作式调度、抢占式调度 协作式调度:程序完成自身任务之后,主动让出资源,缺点是可能会使其它程序得不到资源 抢占式调度:以时间片为单位,程序去占用资源,到时间后被迫让出资源,频繁切换上下文,性能损耗 补充:协程拥有独立栈空间,共享程序堆空间
内存逃逸
1. 本该分配到栈上的变量分配到了堆上 2. 栈是从高地址到低地址分配连续空间,函数结束回收 3. 堆是从低地址到高地址分配,空间不连续,产生内存碎片,堆内存的回收需要gc,会带来性能开销 4. 引起的关键是:编译器在编译阶段无法确认变量生命周期
例子
调用interface{}方法、函数返回局部变量指针、栈空间不足、发指针或带指针数据进channel、切片存指针、切片扩容
逃逸分析
编译器根据代码特征 和 生命周期,自动把变量分配到堆或栈上,优化内存管理机制(被外部引用-》堆 没被外部引用-》栈 栈空间不足-》堆) 好处:把不需要分配到堆上的变量分配到栈上,减少gc压力,提高内存利用效率,减轻内存分配开销 补充:函数传递指针比传值效率高? 不一定,传指针虽可以减少值拷贝,但传指针会发生指针逃逸,使用堆,增加gc负担
内存泄漏
动态分配堆内存由于某种原因无法释放
场景
死锁或互斥锁未释放 协程阻塞无法退出,不释放资源(channel缓存满、死循环)
GC
标记清除1.3: 标记阶段:从程序根节点向下遍历,标记可达对象(根节点:全局变量、局部变量) 清理阶段:清除不可达对象,清除标记 缺点:暂停程序(STW)时间长+ 扫描整个堆栈空间 + 产生堆碎片 三色标记1.5: 白:未搜索到 灰:正在搜索 黑:搜索完毕 最终:灰色对象为0个,清理白色(图解Go的垃圾回收机制_走,我们去吹风的博客-CSDN博客_go 垃圾回收 Golang中的垃圾回收(GC)_Jacson__的博客-CSDN博客_golang内存碎片 三色标记规则:黑色不能指向白色对象 三色标记的扫描线程是跟用户线程并发执行的,回收白色节点前需要STW来重新扫描栈 强三色不变式:不允许黑色对象引用白色对象 弱三色不变式:黑色对象可以引用白色,但也要有其它灰色对象也引用连接它 屏障机制分为插入屏障(强三色不变式)和删除屏障(弱三色不变式) 屏障作用:防止程序运行期间,变量被误回收 插入写屏障:解决黑连白问题,把被指向的对 象标记为灰色 删除写屏障:被删除引用的对象标记为灰色,会造成即使变量被删除也能活过这一轮gc(golang 垃圾回收、三色标记法、写屏障_尖笔尖的博客-CSDN博客_golang三色标记法 屏障只对堆上的内存对象启用 优点:三色标记的扫描线程是跟用户线程并发执行的,增量式运行减少停顿时间
三色标记+混合写屏障1.8: 无需二次扫描,无需STW(stop the world) 在栈所有可达对象均为黑色 被删除、添加引用的对象标记灰色
gc触发时机 手动触发 和 系统触发 (1)手动:runtime.GC(),gcTriggerCycle (2)系统:gcTriggerHeap:大小达到控制器计算阈值 (3)系统:gcTriggerTime:时间间隔达到某值 (默认2分钟) (4)系统:gcTriggerCycle:如果当前没启动,则启动
零gc开销缓存库 https://www.jb51.net/article/237589.htm
锁
mutex
用法:.Lock() .UnLock() 处于之间的部分为临界区 是线程安全的 用处:一般不用做全局变量,一般用在结构体中作为属性的一部分,对锁的操作在协程中进行 缺点:A协程上锁修改资源,B协程如果忘记上锁也是可以修改或访问资源的
type Mutex struct { state int32 //锁:29(正在等待锁协程数)+1(是否被锁lock)+1(是否有协程已被唤醒)+1(是否饥饿,协程阻塞超1ms) sema uint32 //信号量 }
协程解锁:解锁lock=0,释放信号量sema唤醒等待的协程 协程获取锁: 正常模式:新协程A不会立刻加入阻塞队列,会自旋(持续试探锁是否lock几次)尝试抢锁,若此时锁被释放,信号量唤醒阻塞队列的协程B,AB竞争 饥饿模式:新协程A不自旋,加入阻塞队列,信号量唤醒阻塞队列协程
RWmutex
在mutex基础上,增加读写信号量,统计读锁个数 用法:.Lock() .UnLock() .RLock() .RUnLock() (无写锁.UnLock()会panic,无读锁.RUnLock会panic
type RWMutex struct { w Mutex // held if there are pending writers writerSem uint32 // semaphore for writers to wait for completing readers (写协程等读协程释放信号量) readerSem uint32 // semaphore for readers to wait for completing writers (读协程等写协程释放信号量) readerCount int32 // number of pending readers(读协程个数) readerWait int32 // number of departing readers(写阻塞时的读协程个数) }
context
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
告诉子协程和子子协程什么时候结束(例子:go 中的 Context 用法 - 沧海一声笑rush - 博客园 go之Context基本使用 - レモン - 博客园)
func worker(ctx context.Context) { defer wg.Done() LABEL: for { fmt.Println("worker......") time.Sleep(time.Second) select { case <-ctx.Done(): //对于没有缓冲区的 chan,如果后面没有人接,那么就要直接跳过。 break LABEL default: } } } var wg sync.WaitGroup func main() { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() //关闭 wg.Wait() // 等待 fmt.Println("over") }
WaitGroup VS errgroup
WaitGroup
相当于是条件断点,当其他的协程结束后,主协程才结束(因为主函数不会等待子协程结束才结束) 用法:Add() Done() Wait() 注意计数器不能设置为负数 否则panic
type WaitGroup struct { // 避免赋值使用的一个技巧,可以告诉vet工具违反了赋值使用的规则 // noCopy noCopy // 64bit(8bytes)的值分成两段,搞32bit是计数值,低32bit是waiter的计数 // 另外32bit是用信号量的 // 因为64bit值的原子操作需要64bit对齐,但是32bit编译器不支持,所以数组中的元素在不同的架构中不一样,具体处理看下面的方法 // 总之,会找到对齐的那64bit作为state,其余的32bit做信号量 state1 [3]uint32 }
errgroup
具有错误处理功能的waitgroup,errgroup:并发任务 goroutine 的传播控制_机器铃砍菜刀的博客-CSDN博客
// A Group is a collection of goroutines working on subtasks that are part of // the same overall task. // // A zero Group is valid and does not cancel on error. type Group struct { cancel func() wg sync.WaitGroup errOnce sync.Once err error }
pprof
内存泄露 cpu 协程阻塞分析 互斥锁分析(go 程序性能调优 pprof 的使用 (一) - 走看看 go tool pprof http://localhost:6060/debug/pprof/
go tool pprof http://localhost:6060/debug/pprof/ #所有过去内存分配的采样 go tool pprof http://127.0.0.1:8080/debug/pprof/allocs #对活动对象的内存分配进行采样(活动) go tool pprof http://127.0.0.1:8080/debug/pprof/heap # 下载 cpu profile,默认从当前开始收集 30s 的 cpu 使用情况,需要等待 30s go tool pprof http://127.0.0.1:8080/debug/pprof/profile # wait 120s go tool pprof http://127.0.0.1:8080/debug/pprof/profile?seconds=120 #导致同步原语阻塞的堆栈跟踪 go tool pprof http://127.0.0.1:8080/debug/pprof/block #所有当前goroutine的堆栈跟踪 go tool pprof http://127.0.0.1:8080/debug/pprof/goroutine #争用互斥锁持有者的堆栈跟踪 go tool pprof http://127.0.0.1:8080/debug/pprof/mutex #当前程序的执行轨迹。 go tool pprof http://127.0.0.1:8080/debug/pprof/trace
高并发
补充:2种并发模型
1. 线程-锁并发模型:通过共享内存、锁、消息队列实现通信 1. CSP(通信顺序进程模型):goroutine channel select,通过通信来共享内存
保证并发安全方法:
1. **读写锁**:获取不到互斥锁的协程会阻塞,获取不到读写锁的资源会自旋。lock上锁过程调用原子操作CAS,通过信号量来实现线程的阻塞与唤醒;unlock 调用add解锁 2. **原子操作**:不可分割,不会被中断,不会在执行过程中发生(协程的)上下文切换,从而保证并发安全,加锁涉及内核态上下文切换,较耗时。参数是指针类型,指向被操作数内存地址,从而施加特殊的cpu指令(底层硬件指令) 3. **channel** 无缓冲:同步阻塞 有缓冲:不会立刻阻塞
互斥锁VS原子操作 互斥锁是悲观锁,默认值会被修改;原子操作的CAS(compareandswap方法)是乐观锁,如果相同就不交换,默认值不会被修改