golang语言学习

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方法)是乐观锁,如果相同就不交换,默认值不会被修改

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值