怎样用别人的网站做修改病句,网站沙盒期,綦江建站哪家正规,html5动画效果代码目录标题 sliceslice和array的区别slice扩容机制slice是否线程安全slice分配到栈上还是堆上扩容过程中是否重新写入go深拷贝发生在什么情况下#xff1f;切片的深拷贝是怎么做的copy和左值进行初始化区别slice和map的区别 mapmap介绍map的key的类型map对象如何比较map的底层原… 目录标题 sliceslice和array的区别slice扩容机制slice是否线程安全slice分配到栈上还是堆上扩容过程中是否重新写入go深拷贝发生在什么情况下切片的深拷贝是怎么做的copy和左值进行初始化区别slice和map的区别 mapmap介绍map的key的类型map对象如何比较map的底层原理map 负载因子map哈希冲突解决map扩容机制扩容条件增量扩容等量扩容 实现线程安全的mapsync.map的实现场景 map和sync.map的区别map查找过程map插入过程map没申请空间取值会发生什么情况set的原理Java 的HashMap和 go 的map底层原理 channelchannel介绍channel底层实现背景底层结构缓冲区—环形队列等待队列 channel 读写写数据读数据 出现panic的场景出现阻塞的场景channel和锁对比channel应用场景有无缓冲在使用上的区别channel是否线程安全用channel实现分布式锁go channel实现归并排序判断channel已关闭chan和共享内存的优劣势使用chan不占内存空间实现传递信息go中的syncLock和channel的性能区别同一个协程里面对无缓冲channel同时发送和接收数据有什么问题 deferdefer规则defer的执行顺序延迟函数的参数在defer语句出现时就已经确定延迟函数可能操作主函数的具名返回值函数返回过程主函数拥有匿名返回值返回字面值主函数拥有匿名返回值返回变量主函数拥有具名返回值 defer与return谁先谁后defer遇见panicdefer遇见panic但是并不捕获异常的情况defer遇见panic并捕获异常 defer中包含panic 调度模型golang操作内核线程讲一讲GMP模型能开多少个M由什么决定能开多少个M由什么决定golang调度能不能不要P第一版第二版 为什么GMP这么快 GMP调度过程两种类型的队列g阻塞g,m,p发生什么为什么P的local queue可无锁访问任务窃取的时候要加锁吗?go调度中阻塞的方式 具体的调度策略 同时启动一万个G如何调度抢占式调度及goroutine泄漏go的抢占式调度Goroutine 泄露 P和M的数量一定是11吗如果一个G阻塞了会怎么样一个协程挂起换入另外一个协程是什么过程golang gmp模型全局队列中的G会不会饥饿,为什么P的数量是多少能修改吗M的数量是多少第一版第二版 内存管理make和new的异同点内存模型span三级对象管理四级内存块管理 内存分配的实现简单介绍一下go的内存分配机制有mcentral为啥要mcache第一版第二版 GC触发时机go垃圾回收介绍第一版第二版Java垃圾回收golang逃逸分析介绍栈堆逃逸策略逃逸分析好处常见的逃逸现象避免逃逸方法 写代码时如何减少对象分配内存分配和tcmalloc的区别Go 语言内存分配什么分配在堆上什么分配在栈上go性能调优的方法内存优化并发优化其它优化 虚拟内存有什么作用 无效属于操作系统 并发编程说一下reflectruntime提供常见的方法sync.once 如何实现并发安全 context数据结构go 怎么控制查询timeout context go并发优秀在哪里高并发特点 golang并发控制数据安全控制并发行为控制 golang支持哪些并发机制golang中Context的使用场景用共享内存的方式实现并发如何保证安全从运行速度来讲go的并发模型channel和goroutine怎么理解“不要用共享内存来通信而是用通信来共享内存” slice
slice和array的区别
大小固定 vs. 大小可变 数组是大小固定的定义时需要指定数组的长度无法动态增加或减少长度。切片是基于数组的动态长度的抽象可以根据需要动态调整长度。 值传递 vs. 引用传递 数组在赋值或传递时会进行值拷贝即创建一个新的数组副本。切片在赋值或传递时只是传递了一个指向底层数组的引用不会进行拷贝。 定义方式 数组的长度是固定的定义时需要指定长度例如 var arr [5]int。切片的长度是可变的可以通过 make 函数或使用切片字面量定义例如 s : make([]int, 5) 或 s : []int{1, 2, 3}。 内存分配 数组在定义时会直接分配连续的内存空间长度固定。切片在底层依赖数组会根据实际需要动态分配内存空间。 操作和功能 数组具有一些内置的操作和功能如遍历、排序等。切片提供了更多的操作和功能如追加、拼接、截取等。
slice扩容机制
扩容是为切片分配新的内存空间并复制原切片中元素的过程。在 go 语言的切片中扩容的过程是估计大致容量 - 确定容量 - 覆盖原切片 - 完成扩容。
首先判断如果新申请容量大于 2 倍的旧容量最终容量就是新申请的容 量否则判断如果旧切片的长度小于 1024则最终容量就是旧容量的两倍否则判断如果旧切片长度大于等于 1024则最终容量从旧容量开始循环 增加原来的 1/4, 直到最终容量大于等于新申请的容量如果最终容量计算值溢出则最终容量就是新申请容量
slice是否线程安全
Go 的切片slice类型本身并不是线程安全的。多个 goroutine 并发地对同一个切片进行读写操作可能会导致数据竞争和不确定的结果。如果需要在并发环境下安全地使用切片可以采取以下几种方式
使用互斥锁Mutex或读写锁RWMutex来保护对切片的并发访问。在访问切片前获取锁操作完成后释放锁以确保同一时间只有一个 goroutine 可以访问切片。使用通道Channel来进行同步和通信。将切片操作封装为一个独立的 goroutine通过通道接收和发送操作来保证对切片的顺序访问。使用原子操作Atomic Operations来进行原子性的读写操作。Go 提供了一些原子操作的函数如 atomic.AddInt32、atomic.LoadPointer 等可以确保在并发环境下对切片的操作是原子的。
slice分配到栈上还是堆上
有可能分配到栈上也有可能分配到栈上。当开辟切片空间较大时会逃逸到堆上。
扩容过程中是否重新写入
切片的扩容 当在尾部扩容时追加元素不需要重新写入
var a []int
a append(a, 1)在头部插入时会引起内存的重分配导致已有的元素全部重新写入
a append([]int{0}, a...);在中间插入时会局部重新写入如下 使用链式操作在插入元素在内层append函数中会创建一个临式切片然后将a[i:]内容复制到新创建的临式切片中再将临式切片追加至a[:i]中。
a append(a[:i], append([]int{x}, a[i:]...)...)
a append(a[:i], append([]int{1, 2, 3}, a[i:]...)...)//在第i个位置上插入切片go深拷贝发生在什么情况下切片的深拷贝是怎么做的
深拷贝Deep Copy
拷贝的是数据本身创造一个样的新对象新创建的对象与原对象不共享内存新创建的对象在内存中开辟一个新的内存地址新对象值修改时不会影响原对象值。既然内存地址不同释放内存地址时可分别释放。
浅拷贝Shallow Copy
拷贝的是数据地址只复制指向的对象的指针此时新对象和老对象指向的内存地址是一样的新对象值修改时老对象也会变化。释放内存地址时同时释放内存地址。参考来源 (opens new window)在go语言中值类型赋值都是深拷贝引用类型一般都是浅拷贝
值类型的数据默认全部都是深拷贝Array、Int、String、Struct、FloatBool引用类型的数据默认全部都是浅拷贝SliceMap
对于引用类型想实现深拷贝不能直接 : 而是要先开辟地址空间new 再进行赋值。可以使用 copy() 函数对slice进行深拷贝copy 不会进行扩容当要复制的 slice 比原 slice 要大的时候只会移除多余的。使用 append() 函数来进行深拷贝append 会进行扩容
copy和左值进行初始化区别
copy(slice2, slice1)实现的是深拷贝。拷贝的是数据本身创造一个新对象新创建的对象与原对象不共享内存新创建的对象在内存中开辟一个新的内存地址新对象值修改时不会影响原对象值。 同样的还有遍历slice进行append赋值如slice2 : slice1实现的是浅拷贝。拷贝的是数据地址只复制指向的对象的指针此时新对象和老对象指向的内存地址是一样的新对象值修改时老对象也会变化。默认赋值操作就是浅拷贝。
slice和map的区别
Map 是一种无序的键值对的集合。Map 可以通过 key 来快速检索数据key 类似于索引指向数据的值。 而 Slice 是切片可以改变长度动态扩容切片有三个属性指针长度容量。 二者都可以用 make 进行初始化。
map
map介绍
Go中Map是一个KV对集合。底层使用hash table用链表来解决冲突 出现冲突时不是每一个Key都申请一个结构通过链表串起来而是以bmap为最小粒度挂载一个bmap可以放8个kv。每个map的底层结构是hmap是有若干个结构为bmap的bucket组成的数组。每个bucket底层都采用链表结构。bmap 就是我们常说的“桶”桶里面会最多装 8 个 key这些 key之所以会落入同一个桶是因为它们经过哈希计算后哈希结果是“一类”的关于key的定位我们在map的查询和赋值中详细说明。在桶内又会根据key计算出来的hash值的高8位来决定 key到底落入桶内的哪个位置一个桶内最多有8个位置)。
map的key的类型
map[key]value其中key必须是可比较的也就是可以通过和!进行比较所以可以比较的类型才能作为key其实就是等价问go语言中哪些类型是可以比较的
什么可以比较bool、array、numeric浮点数、整数等、pointer、string、interface、channel
什么不能比较function、slice、map
golang中的map的 key 可以是很多种类型比如 bool, 数字string, 指针, channel , 还有 只包含前面几个类型的 interface types, structs, arrays map是可以进行嵌套的。
map对象如何比较
使用reflect.DeepEqual 这个函数进行比较。使用 reflect.DeepEqual 有一点注意由于使用了反射所以有性能的损失。
map的底层原理
map的实现原理 go map是基于hash table哈希表来实现的冲突的解决采用拉链法 map的底层结构 hmap哈希表每个hmap内含有多个bmapbuckets桶、oldbuckets旧桶、overflow溢出桶可以这样理解每个哈希表都是由多个桶组成的
type hmap struct {count int //元素的个数flags uint8 //状态标志B uint8 //可以最多容纳 6.5 * 2 ^ B 个元素6.5为装载因子noverflow uint16 //溢出的个数hash0 uint32 //哈希种子buckets unsafe.Pointer //指向一个桶数组oldbuckets unsafe.Pointer //指向一个旧桶数组用于扩容nevacuate uintptr //搬迁进度小于nevacuate的已经搬迁overflow *[2]*[]*bmap //指向溢出桶的指针
} buckets一个指针指向一个bmap数组、存储多个桶。oldbuckets 是一个指针指向一个bmap数组存储多个旧桶用于扩容。overflowoverflow是一个指针指向一个元素个数为2的数组数组的类型是一个指针指向一个sliceslice的元素是桶(bmap)的地址这些桶都是溢出桶。为什么有两个因为Go map在哈希冲突过多时会发生扩容操作。[0]表示当前使用的溢出桶集合[1]是在发生扩容时保存了旧的溢出桶集合。overflow存在的意义在于防止溢出桶被gc。 bmap哈希桶 bmap是一个隶属于hmap的结构体一个桶bmap可以存储8个键值对。如果有第9个键值对被分配到该桶那就需要再创建一个桶通过overflow指针将两个桶连接起来。在hmap中多个bmap桶通过overflow指针相连组成一个链表。
type bmap struct {//元素hash值的高8位代表它在桶中的位置如果tophash[0] minTopHash表示这个桶的搬迁状态tophash [bucketCnt]uint8//接下来是8个key、8个value但是我们不能直接看到为了优化对齐go采用了key放在一起value放在一起的存储方式keys [8]keytype //key单独存储values [8]valuetype //value单独存储pad uintptroverflow uintptr //指向溢出桶的指针
}map 负载因子
负载因子用于衡量一个哈希表冲突情况公式为
负载因子 键数量/bucket数量例如对于一个bucket数量为4包含4个键值对的哈希表来说这个哈希表的负载因子为1.哈希表需要将负载因子控制在合适的大小超过其阀值需要进行rehash也即键值对重新组织
哈希因子过小说明空间利用率低哈希因子过大说明冲突严重存取效率低
每个哈希表的实现对负载因子容忍程度不同比如Redis实现中负载因子大于1时就会触发rehash而Go则在在负载因子达到6.5时才会触发rehash因为Redis的每个bucket只能存1个键值对而Go的bucket可能存8个键值对所以Go可以容忍更高的负载因子。
map哈希冲突解决
在Go语言中普通的map类型在哈希冲突的情况下采用了开链法链地址法来解决。当不同的键经过哈希计算后映射到了同一个桶bucket时就会产生哈希冲突。为了解决这些冲突每个桶会维护一个链表将哈希值相同的键值对链接在一起。以下是哈希冲突如何在Go中的普通map中解决的简要过程
哈希计算当插入或查找一个键值对时首先会对键进行哈希计算得到一个哈希值。映射到桶哈希值会被映射到一个特定的桶。Go中的map底层使用了一个哈希表这个哈希表由多个桶组成。处理冲突如果两个不同的键的哈希值映射到了同一个桶就会发生哈希冲突。此时系统会将新的键值对添加到该桶对应的链表中。链表操作链表中的每个节点代表一个键值对相同哈希值的键值对会链接在同一个桶的链表上。插入时会在链表头部插入节点这使得查找和删除操作的时间复杂度相对较低。查找和删除对于查找操作系统会先计算哈希值并找到对应的桶然后遍历该桶的链表以找到目标键值对。对于删除操作会在链表中找到目标键值对并将其从链表中移除
map扩容机制
扩容条件
负载因子大于6.5时负载因子 键数量/bucket数量overflow的数量达到2^min(B,15)时
增量扩容
新建一个bucket数组新的bucket数组的长度是原来的两倍然后旧bucket数组中的数据搬迁到新的bucket数组中。考虑到如果map存储了数以亿计的key-value一次性搬迁将会造成比较大的延时Go采用逐步搬迁策略即每次访问map时都会触发一次搬迁每次搬迁2个键值对。
等量扩容
所谓等量扩容实际上并不是扩大容量buckets数量不变重新做一遍类似增量扩容的搬迁动作把松散的键值对重新排列一次以使bucket的使用率更高进而保证更快的存取。
实现线程安全的map
Map默认不是并发安全的并发读写时程序会panic。map为什么不支持线程安全和场景有关官方认为大部分场景不需要多个协程进行并发访问如果为小部分场景加锁实现并发访问大部分场景将付出加锁代价性能降低。
加读写锁分片加锁sync.Map
加读写锁、分片加锁这两种方案都比较常用后者的性能更好因为它可以降低锁的粒度提高访问此 map 对象的吞吐。前者并发性能虽然不如后者 但是加锁的方式更加简单。sync.Map 是 Go 1.9 增加的一个线程安全的 map 虽然是官方标准但反而是不常用的原因是 map 要解决的场景很难 描述很多时候程序员在做抉择是否该用它不过在一些特殊场景会使用 sync.Map场景一只会增长的缓存系统一个 key 值写入一次而被读很多次 场景二多个 goroutine 为不相交的键读、写和重写键值对。对它的使用场景介绍来自官方文档 (opens new window)这里就不展开了。 加读写锁扩展 map 来实现线程安全支持并发读写。使用读写锁 RWMutex是为了读写性能的考虑。 对 map 对象的操作无非就是常见的增删改查和遍历。我们可以将查询和遍历看作读操作增加、修改和 删除看作写操作。示例代码链接https://github.com/guowei-gong/go-demo/blob/main/mutex/demo.go 。通过读写锁提供线程安全的 map但是大量并发读写的情况下锁的竞争会很激烈导致性能降低。如何解决这个问题 尽量减少锁的粒度和锁的持有时间减少锁的粒度常用方法就是分片 Shard将一把锁分成几把锁每个锁控制一个分片。
sync.map的实现
sync.map采用读写分离和用空间换时间的策略保证map的读写安全。
散列桶和片段划分sync.Map的底层使用了一个散列桶数组来存储键值对。这个数组被划分成多个小的片段每个片段有自己的锁这样不同的片段可以独立地进行操作从而减少了竞争。读写分离为了允许高并发读取sync.Map实现了一种读写分离的机制。在读取时不需要锁定多个goroutine可以并发读取。写操作涉及到写入数据会获取特定散列桶的写锁。散列算法和冲突解决sync.Map使用散列算法将键映射到散列桶。每个散列桶中都可能包含多个键值对因此可能会出现散列冲突。冲突的解决方式通常是通过链表来存储具有相同散列的键值对。版本控制sync.Map引入了版本控制的概念。每个散列桶中都包含了一个版本号用于跟踪对散列桶的修改。这使得在读取时可以检测到同时进行的写入从而确保读取的数据的一致性。内存管理和垃圾回收sync.Map还包含了一些内存管理机制以避免不再使用的内存积累。当某个散列桶不再被使用时相应的内存可能会被释放。
基本结构
type Map struct {mu Mutexread atomic.Value //包含对并发访问安全的map内容的部分(无论是否持有mu)dirty map[ant]*entry //包含map内容中需要保存mu的部分misses int //计算自从上次读取map更新后需要锁定mu来确定key是否存在的加载次数
}readread 使用 map[any]*entry 存储数据本身支持无锁的并发读read 可以在无锁的状态下支持 CAS 更新但如果更新的值是之前已经删除过的 entry 则需要加锁操作由于 read 只负责读取dirty 负责写入因此使用 amended 来标记 dirty 中是否包含 read 没有的字段
**dirty**dirty 本身就是一个原生 map需要加锁保证并发写入
**entry**read 和 dirty 都是用到 entry 结构entry 内部只有一个 unsafe.Pointer 指针 p 指向 entry 实际存储的值指针 p 有三种状态 p nil 在此状态下对应 entry 已经被删除 或 map.dirty nil 或 map.dirty 中有 key 指向 e 此处不明 p expunged 在此状态下对应entry 已经被删除 或 map.dirty ! nil 同时该 entry 无法在 dirty 中找到 其他情况 entry 都是有效状态并被记录在 read 中如果 dirty 不为空则也可以在 dirty 中找到
场景
只会增长的缓存系统一个key只写一次而被读很多次多个goroutine为不相交的键集读、写和重写键值对
map和sync.map的区别
线程安全性map 是非线程安全的多个 goroutine 并发地读写 map 可能会导致数据竞争和不确定的结果。而 sync.Map 是线程安全的可以在多个 goroutine 并发地读写 sync.Map而不需要额外的同步操作。扩容机制map 的扩容是在插入新元素时自动进行的按需增加内部哈希表的大小。而 sync.Map 不会自动扩容它始终使用固定大小的内部哈希表。功能和方法map 提供了常见的读取、插入、更新和删除等操作如 m[key]、m[key] value、delete(m, key) 等。而 sync.Map 提供了一组特定的方法如 Load、Store、Delete 和 Range用于读取、存储、删除和遍历键值对。性能由于 sync.Map 是线程安全的它需要进行额外的同步操作因此在并发性能方面可能会比普通的 map 稍慢。而普通的 map 在单个 goroutine 下的读取和写入操作性能较高
map查找过程
查找过程如下
根据key值算出哈希值取哈希值低位与hmap.B取模确定bucket位置取哈希值高位在tophash数组中查询如果tophash[i]中存储值也哈希值相等则去找到该bucket中的key值进行比较当前bucket没有找到则继续从下个overflow的bucket中查找。如果当前处于搬迁过程则优先从oldbuckets查找
注如果查找不到也不会返回空值而是返回相应类型的0值。
map插入过程
新元素插入过程如下
根据key值算出哈希值取哈希值低位与hmap.B取模确定bucket位置查找该key是否已经存在如果存在则直接更新值如果没找到将key将key插入
map没申请空间取值会发生什么情况
在map查询操作中最多可以给两个变量赋值第一个为值第二个为bool类型的变量用于指示是否存在指定的键如果键不存在那么第一个值为相应类型的零值。如果只指定一个变量那么该变量仅表示改键对应的值如果键不存在那么该值同样为相应类型的零值。
set的原理Java 的HashMap和 go 的map底层原理
1. Set原理: Set特性: 1. 不包含重复key. 2.无序. 如何去重: 通过查看源码add(E e)方法底层实现有一个mapmap是HashMap,Hash类型是散列所以是无序的. 如果key值相同将会覆盖这就是set为什么能去重的原因(key相同会覆盖). 注意: 如果new出两个对象add到set中,因为两个对象的地址不相同,所以map在计算key的hash值时将它当成了两个不同的元素。这时要重写equals和hashcode两个方法。 这样才能保证set集合的元素不重复.
2. Java HashMap:
线程不安全 安全的map(CurrentHashMap) HashMap由数组链表组成,数组是HashMap的主体, 链表则是为了解决哈希冲突而存在的,如果定位到的数组位置不含链表当前entry的next指向null,那么查找添加等操作很快仅需一次寻址即可 如果定位到的数组包含链表对于添加操作其时间复杂度为O(n)首先遍历链表存在即覆盖否则新增 对于查找操作来讲仍需遍历链表然后通过key对象的equals方法逐一比对查找。 所以性能考虑HashMap中的链表出现越少性能才会越好。 假如一个数组槽位上链上数据过多即链表过长的情况导致性能下降该怎么办 JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。 即当链表超过8时链表就转换为红黑树利用红黑树快速增删改查的特点提高HashMap的性能其中会用到红黑树的插入、删除、查找等算法。
3. go map:
线程不安全 安全的map(sync.map) 特性: 1. 无序. 2. 长度不固定. 3. 引用类型. 底层实现: 1.hmap 2.bmap(bucket) hmap中含有n个bmap是一个数组. 每个bucket又以链表的形式向下连接新的bucket. bucket关注三个字段: 1. 高位哈希值 2. 存储key和value的数组 3. 指向扩容bucket的指针 高位哈希值: 用于寻找bucket中的哪个key. 低位哈希值: 用于寻找当前key属于hmap中的哪个bucket. map的扩容: 当map中的元素增长的时候Go语言会将bucket数组的数量扩充一倍产生一个新的bucket数组并将旧数组的数据迁移至新数组。 加载因子 判断扩充的条件就是哈希表中的加载因子(即loadFactor)。 加载因子是一个阈值一般表示为散列包含的元素数 除以 位置总数。是一种“产生冲突机会”和“空间使用”的平衡与折中加载因子越小说明空间空置率高空间使用率小但是加载因子越大说明空间利用率上去了但是“产生冲突机会”高了。 每种哈希表的都会有一个加载因子数值超过加载因子就会为哈希表扩容。 Golang的map的加载因子的公式是map长度 / 2^B(这是代表bmap数组的长度B是取的低位的位数)阈值是6.5。其中B可以理解为已扩容的次数。 当Go的map长度增长到大于加载因子所需的map长度时Go语言就会将产生一个新的bucket数组然后把旧的bucket数组移到一个属性字段oldbucket中。注意并不是立刻把旧的数组中的元素转义到新的bucket当中而是只有当访问到具体的某个bucket的时候会把bucket中的数据转移到新的bucket中。 map删除: 并不会直接删除旧的bucket而是把原来的引用去掉利用GC清除内存。
channel
channel介绍
channel是Golang在语言层面提供的goroutine间的通信方式channel主要用于进程内各goroutine间的通信。channel分为无缓冲channel和有缓冲channel。
Channel 在 gouroutine 间架起了一条管道在管道里传输数据实现 gouroutine 间的通信在并发编程中它线程安全的所以用起来非常方便channel 还提供“先进先出”的特性它还能影响 goroutine 的阻塞和唤醒。
channel底层实现
背景
Go语言提供了一种不同的并发模型–通信顺序进程(communicating sequential processes,CSP)。设计模式通过通信的方式共享内存channel收发操作遵循先进先出(FIFO)的设计
底层结构
type hchan struct {qcount uint // 当前队列中剩余元素个数dataqsiz uint // 环形队列长度即可以存放的元素个数buf unsafe.Pointer // 环形队列指针elemsize uint16 // 每个元素的大小closed uint32 // 标识关闭状态elemtype *_type // 元素类型sendx uint // 队列下标指示元素写入时存放到队列中的位置recvx uint // 队列下标指示元素从队列的该位置读出recvq waitq // 等待读消息的goroutine队列sendq waitq // 等待写消息的goroutine队列lock mutex // 互斥锁chan不允许并发读写
}从数据结构可以看出channel由队列、类型信息、goroutine等待队列组成channel内部数据结构主要包含
环形队列等待队列(读队列和写队列)mutex
缓冲区—环形队列
chan内部实现了一个环形队列作为其缓冲区队列的长度是创建chan时指定的。 dataqsiz指示了队列长度为6即可缓存6个元素buf指向队列的内存队列中还剩余两个元素qcount表示队列中还有两个元素sendx指示后续写入的数据存储的位置取值[0, 6)recvx指示从该位置读取数据, 取值[0, 6)
等待队列
从channel读数据如果channel缓冲区为空或者没有缓冲区当前goroutine会被阻塞。 向channel写数据如果channel缓冲区已满或者没有缓冲区当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中
因读阻塞的goroutine会被向channel写入数据的goroutine唤醒因写阻塞的goroutine会被从channel读数据的goroutine唤醒
channel 读写
写数据
向一个channel中写数据简单过程如下
如果等待接收队列recvq不为空说明缓冲区中没有数据或者没有缓冲区此时直接从recvq取出G,并把数据写入最后把该G唤醒结束发送过程如果缓冲区中有空余位置将数据写入缓冲区结束发送过程如果缓冲区中没有空余位置将待发送数据写入G将当前G加入sendq进入睡眠等待被读goroutine唤醒
读数据
如果等待发送队列sendq不为空且没有缓冲区直接从sendq中取出G把G中数据读出最后把G唤醒结束读取过程如果等待发送队列sendq不为空此时说明缓冲区已满从缓冲区中首部读出数据把G中数据写入缓冲区尾部把G唤醒结束读取过程如果缓冲区中有数据则从缓冲区取出数据结束读取过程将当前goroutine加入recvq进入睡眠等待被写goroutine唤醒
出现panic的场景
关闭channel时会把recvq中的G全部唤醒本该写入G的数据位置为nil。把sendq中的G全部唤醒但这些G会panic。
除此之外panic出现的常见场景还有
关闭值为nil的channel关闭已经被关闭的channel向已经关闭的channel写数据
出现阻塞的场景
无缓冲区读写数据会阻塞缓冲区已满写入会阻塞缓冲区为空读取数据会阻塞值为nil读写数据会阻塞
channel和锁对比
并发问题可以用channel解决也可以用Mutex解决但是它们的擅长解决的问题有一些不同。channel关注的是并发问题的数据流动适用于数据在多个协程中流动的场景。而mutex关注的是是数据不动某段时间只给一个协程访问数据的权限适用于数据位置固定的场景。
channel应用场景
channel适用于数据在多个协程中流动的场景有很多实际应用
定时任务超时处理解耦生产者和消费者可以将生产者和消费者解耦出来生产者只需要往channel发送数据而消费者只管从channel中获取数据。控制并发数以爬虫为例比如需要爬取1w条数据需要并发爬取以提高效率但并发量又不过过大可以通过channel来控制并发规模比如同时支持5个并发任务
有无缓冲在使用上的区别
无缓冲发送和接收需要同步。 有缓冲不要求发送和接收同步缓冲满时发送阻塞。 因此 channel 无缓冲时发送阻塞直到数据被接收接收阻塞直到读到数据channel有缓冲时当缓冲满时发送阻塞当缓冲空时接收阻塞。
channel是否线程安全
channel为什么设计成线程安全? 不同协程通过channel进行通信本身的使用场景就是多线程为了保证数据的一致性必须实现线程安全。channel如何实现线程安全的? channel的底层实现中 hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时必须先获取互斥锁才能操作channel数据。
用channel实现分布式锁
分布式锁定义-控制分布式系统有序的去对共享资源进行操作通过互斥来保持一致性。 通过数据库rediszookeeper都可以实现分布式锁。其中最常见的是用redis的setnx实现。
通过channel作为媒介利用struct{}{}作为信号判断struct{}{}是否存在进行加锁、解锁操作。
go channel实现归并排序
func Merge(ch1 -chan int, ch2 -chan int) -chan int {out : make(chan int)go func() {// 等上游的数据 这里有阻塞和常规的阻塞队列并无不同v1, ok1 : -ch1v2, ok2 : -ch2// 取数据for ok1 || ok2 {if !ok2 || (ok1 v1 v2) {// 取到最小值, 就推到 out 中out - v1v1, ok1 -ch1} else {out - v2v2, ok2 -ch2}}// 显式关闭close(out)}()// 开完goroutine后, 主线程继续执行, 不会阻塞return out判断channel已关闭
方式1通过读chennel实现
用 select 和 -ch 来结合判断ok的结果和含义 true读到数据并且通道 (opens new window)没有关闭。 false通道关闭无数据读到。需要注意 1.case 的代码必须是 _, ok: - ch 的形式如果仅仅是 - ch 来判断是错的逻辑因为主要通过 ok的值来判断 2.select 必须要有 default 分支否则会阻塞函数我们要保证一定能正常返回
方式2通过context
通过一个 ctx 变量来指明 close 事件而不是直接去判断 channel 的一个状态. 当ctx.Done()中有值时则判断channel已经退出。注意: select 的 case 一定要先判断 ctx.Done() 事件否则很有可能先执行了 chan 的操作从而导致 panic 问题
chan和共享内存的优劣势
Go的设计思想就是, 不要通过共享内存来通信而是通过通信来共享内存前者就是传统的加锁后者就是Channel。 共享内存是在操作内存的同时通过互斥锁、CAS等保证并发安全而channel虽然底层维护了一个互斥锁来保证线程安全但其可以理解为先进先出的队列通过管道进行通信。 共享内存优势是资源利用率高、系统吞吐量大,劣势是平均周转时间长、无交互能力。 channel优势是降低了并发中的耦合劣势是会出现死锁。
使用chan不占内存空间实现传递信息
// 空结构体的宽度是0占用了0字节的内存空间。
// 所以空结构体组成的组合数据类型也不会占用内存空间。
channel : make(chan struct{})
go func() {// do somethingchannel - struct{}{}
}()
fmt.Println(-channel)
go中的syncLock和channel的性能区别
hannel的底层也是用了syns.Mutex,算是对锁的封装性能应该是有损耗的。根据压测结果来说Mutex 比 channel的性能快了两倍左右
同一个协程里面对无缓冲channel同时发送和接收数据有什么问题
同一个协程里不能对无缓冲channel同时发送和接收数据如果这么做会直接报错死锁。对于一个无缓冲的channel而言只有不同的协程之间一方发送数据一方接受数据才不会阻塞。channel无缓冲时发送阻塞直到数据被接收接收阻塞直到读到数据。
defer
defer规则
defer的执行顺序
多个defer出现的时候它是一个“栈”的关系也就是先进后出。一个函数中写在前面的defer会比写在后面的defer调用的晚。
延迟函数的参数在defer语句出现时就已经确定
func a() {i : 0defer fmt.Println(i)ireturn
}defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行仍然打印”0”。注意对于指针类型参数规则仍然适用只不过延迟函数的参数是一个地址值这种情况下defer后面的语句对变量的修改可能会影响延迟函数。
延迟函数可能操作主函数的具名返回值
定义defer的函数即主函数可能有返回值返回值有没有名字没有关系defer所作用的函数即延迟函数可能会影响到返回值。若要理解延迟函数是如何影响主函数返回值的只要明白函数是如何返回的就足够了。
函数返回过程
关键字return不是一个原子操作实际上return只代理汇编指令ret即将跳转程序执行。比如语句return i实际上分两步进行即将i值存入栈中作为返回值然后执行跳转而defer的执行时机正是跳转前所以说defer执行时还是有机会操作返回值的。
主函数拥有匿名返回值返回字面值
一个主函数拥有一个匿名的返回值返回时使用字面值比如返回”1”、”2”、”Hello”这样的值这种情况下defer语句是无法操作返回值的
func f() int {var i intdefer func() {i}()return 2
}
// 上面的return语句直接把1写入栈中作为返回值延迟函数无法操作该返回值所以就无法影响返回值。主函数拥有匿名返回值返回变量
一个主函数拥有一个匿名的返回值返回使用本地或全局变量这种情况下defer语句可以引用到返回值但不会改变返回值。
func f() int {var i intdefer func() {i}()return i
}
// 上面的函数返回一个局部变量同时defer函数也会操作这个局部变量。对于匿名返回值来说可以假定仍然有一个变量存储返回值假定返回值变量为”anony”上面的返回语句可以拆分成以下过程
anony i
i
return
// 由于i是整型会将值拷贝给anony所以defer语句中修改i值对函数返回值不造成影响。主函数拥有具名返回值
主函声明语句中带名字的返回值会被初始化成一个局部变量函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值可能会改变返回结果。一个影响函返回值的例子
func foo() (ret int) {defer func() {ret}()return 0
}
// 上面的函数拆解出来如下所示ret 0
ret
return
// 函数真正返回前在defer中对返回值做了1操作所以函数最终返回1。defer与return谁先谁后
return之后的语句先执行defer后的语句后执行
defer遇见panic
能够触发defer的是遇见return(或函数体到末尾)和遇见panic。defer遇见return情况如下 遇到panic时遍历本协程的defer链表并执行defer。在执行defer过程中:遇到recover则停止panic返回recover处继续往下执行。如果没有遇到recover遍历完本协程的defer链表后向stderr抛出panic信息。
defer遇见panic但是并不捕获异常的情况
package mainimport (fmt
)
func main() {deferTest()fmt.Println(main 正常结束)
}
func deferTest() {defer func() { fmt.Println(defer: panic 之前1) }()defer func() { fmt.Println(defer: panic 之前2) }()panic(异常内容) //触发defer出栈defer func() { fmt.Println(defer: panic 之后永远执行不到) }()
}defer遇见panic并捕获异常
package mainimport (fmt
)func main() {deferTest()fmt.Println(main 正常结束)
}func deferTest() {defer func() {fmt.Println(defer: panic 之前1, 捕获异常)if err : recover(); err ! nil {fmt.Println(err)}}()defer func() { fmt.Println(defer: panic 之前2, 不捕获) }()panic(异常内容) //触发defer出栈defer func() { fmt.Println(defer: panic 之后, 永远执行不到) }()
}defer 最大的功能是 panic 后依然有效所以defer可以保证你的一些资源一定会被关闭从而避免一些异常出现的问题。
defer中包含panic
package mainimport (fmt
)func main() {defer func() {if err : recover(); err ! nil{fmt.Println(err)}else {fmt.Println(fatal)}}()defer func() {panic(defer panic)}()panic(panic)
}
// 输出 defer panicpanic仅有最后一个可以被revover捕获。触发panic(panic)后defer顺序出栈执行第一个被执行的defer中 会有panic(defer panic)异常语句这个异常将会覆盖掉main中的异常panic(panic)最后这个异常被第二个执行的defer捕获到。
调度模型
golang操作内核线程
在此模型下的用户线程与内核线程一一对应也就是说完全接管了用户线程它也属于内核的一部分统一由调度器来创建、终止和切换。这样就能完全发挥出多核的优势多个线程可以跑在不同的CPU上实现真正的并行。但也正由于一切都由内核来调度这样大大增加了工作量线程的切换是非常耗时的而且创建也很用到更多的资源所以也大大减少能创建线程的数量。由于是一对一的关系所以也叫1:1线程实现。
讲一讲GMP模型
GGoroutineG 就是我们所说的 Go 语言中的协程 Goroutine 的缩写相当于操作系统中的进程控制块。其中存着 goroutine 的运行时栈信息CPU 的一些寄存器的值以及执行的函数指令等。MMachine代表一个操作系统的主线程对内核级线程的封装数量对应真实的 CPU 数。一个 M 直接关联一个 os 内核线程用于执行 G。M 会优先从关联的 P 的本地队列中直接获取待执行的 G。M 保存了 M 自身使用的栈信息、当前正在 M上执行的 G 信息、与之绑定的 P 信息。PProcessorProcessor 代表了 M 所需的上下文环境代表 M 运行 G 所需要的资源。是处理用户级代码逻辑的处理器可以将其看作一个局部调度器使 go 代码在一个线程上跑。当 P 有任务时就需要创建或者唤醒一个系统线程来执行它队列里的任务所以 P 和 M 是相互绑定的。总的来说P 可以根据实际情况开启协程去工作它包含了运行 goroutine 的资源如果线程想运行 goroutine必须先获取 PP 中还包含了可运行的 G 队列。
能开多少个M由什么决定
由于M必须持有一个P才可以运行Go代码所以同时运行的M个数也即线程数一般等同于CPU的个数以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。P的个数默认等于CPU核数每个M必须持有一个P才可以执行G一般情况下M的个数会略大于P的个数这多出来的M将会在G产生系统调用时发挥作用。Go语⾔本身是限定M的最⼤量是10000可以在runtime/debug包中的SetMaxThreads函数来修改设置
能开多少个M由什么决定
P的个数在程序启动时决定默认情况下等同于CPU的核数程序中可以使用 runtime.GOMAXPROCS() 设置P的个数在某些IO密集型的场景下可以在一定程度上提高性能。一般来讲程序运行时就将GOMAXPROCS大小设置为CPU核数可让Go程序充分利用CPU。在某些IO密集型的应用里这个值可能并不意味着性能最好。理论上当某个Goroutine进入系统调用时会有一个新的M被启用或创建继续占满CPU。但由于Go调度器检测到M被阻塞是有一定延迟的也即旧的M被阻塞和新的M得到运行之间是有一定间隔的所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些或许会有好的效果。
golang调度能不能不要P
第一版
1.介绍golang调度器中P是什么
Processor的简称处理器上下文。
2.简述p的功能与为什么必须要P
它的主要用途就是用来执行goroutine的它维护了一个goroutine队列。Processor是让咱们从N:1调度到M:N调度的重要部分
第二版
在 Go 语言中PProcessor是调度器的一部分用于管理和执行 goroutine。每个 P 都有一个固定的系统线程OS thread关联用于在该线程上执行 goroutine。P 的存在是为了协调调度器和系统线程之间的关系它充当了调度器和操作系统之间的中间层。P 的作用包括
调度P 负责将 goroutine 分配给系统线程执行并在系统线程空闲时重新分配。Goroutine 栈管理P 管理 goroutine 的栈空间包括分配和回收。垃圾回收P 参与垃圾回收过程协助标记和清理不再使用的内存。
由于 Go 语言的调度器是基于 M:N 模型实现的即将 M 个 goroutine 关联到 N 个系统线程上执行因此不能直接在没有 P 的情况下运行 goroutine。
为什么GMP这么快
谈到 Go 语言调度器绕不开操作系统进程与线程这些概念。线程是操作系统调度的最小单元而 Linux 调度器并不区分进程和线程的调度它们在不同操作系统上的实现也不同但是在大多数实现中线程属于进程。多个线程可以属于同一个进程并共享内存空间。因为多线程不需要创建新的虚拟内存空间所以它们也不需要内存管理单元处理上下文的切换线程之间的通信也正是基于共享内存进行的与重量级进程相比线程显得比较轻量。虽然线程比较轻量但是在调度时也有比较大的额外开销。每个线程会都占用 1MB 以上的内存空间在切换线程时不止会消耗较多内存恢复寄存器中的内存还需要向操作系统申请或者销毁资源。每一个线程上下文的切换都需要消耗 1 us 的时间而 Go 调度器对 Goroutine 的上下文切换越为 0.2us减少了 80% 的额外开销。Go 语言的调度器使用与 CPU 数量相等的线程来减少线程频繁切换带来的内存开销同时在每一个线程上执行额外开销更低的 Goroutine 来降低操作系统和硬件的负载。
GMP调度过程
我们通过 go func()来创建一个goroutine有两个存储G的队列一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中如果P的本地队列已经满了就会保存在全局的队列中G只能运行在M中一个M必须持有一个PM与P是11的关系。M会从P的本地队列弹出一个可执行状态的G来执行如果P的本地队列为空会从全局队列拿P如果全局队列也为空就会向其他的MP组合偷取一个可执行的G来执行一个M调度G执行的过程是一个循环机制当M执行某一个G时候如果发生了syscall或则其余阻塞操作M会阻塞如果当前有一些G在执行runtime会把这个线程M从P中摘除(detach)然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P当M系统调用结束时候这个G会尝试获取一个空闲的P执行并放入到这个P的本地队列。如果获取不到P那么这个线程M变成休眠状态 加入到空闲线程中然后这个G会被放入全局队列中。
两种类型的队列
本地队列本地的队列是无锁的没有数据竞争问题处理速度比较高。全局队列是用来平衡不同的P的任务数量所有的M共享P的全局队列。全局G队列Global Queue存放等待运⾏的G。P的本地G队列同全局队列类似存放的也是等待运⾏的G存的数量有限不超过256个。 新建G时G优先加入到P的本地队列如果队列满了则会把本地队列中⼀半的G移动到全局队列P列表所有的P都在程序启动时创建并保存在数组中最多有 GOMAXPROCS(可配置)个。可通过 runtime.GOMAXPROCS() 来进⾏设置1.5版本之前默认为1使⽤单核⼼执⾏之后默认为最⼤逻辑cpu数量即默认有最⼤逻辑cpu数量个P。、M列表当前操作系统分配给golang程序的内核线程数。线程想运⾏任务就得获取P从P的本地队列获取GP队列为空时M会优先尝试从全局队列拿⼀批G放到P的本地队列或从其他P的本地队列偷⼀半放到⾃⼰P的本地队列。M运⾏GG执⾏之后M会从P获取下⼀个G不断重复下去。 Goroutine调度器和OS调度器是通过M结合起来的每个M都代表了1个内核线程OS调度器负责把内核线程分配到CPU的
g阻塞g,m,p发生什么
当g阻塞时p会和m解绑去寻找下一个可用的m。 gm在阻塞结束之后会优先寻找之前的p如果此时p已绑定其他m当前m会进入休眠g以可运行的状态进入全局队列
为什么P的local queue可无锁访问任务窃取的时候要加锁吗?
绑定在P上的local queue是顺序执行的不存在执行状态的G协程抢占所以可以无锁访问。任务窃取也是窃取其他P上等待状态的G协程所以也可以不用加锁。
go调度中阻塞的方式
由于原子、互斥量或通道操作调用导致 Goroutine 阻塞调度器将把当前阻塞的 Goroutine 切换出去重新调度 LRQ 上的其他 Goroutine由于网络请求和 IO 操作导致 Goroutine 阻塞。Go 程序提供了网络轮询器NetPoller来处理网络请求和 IO 操作的问题其后台通过 kqueueMacOSepollLinux或 iocpWindows来实现 IO 多路复用。通过使用 NetPoller 进行网络系统调用调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines而不需要创建新的 M。执行网络系统调用不需要额外的 M网络轮询器使用系统线程它时刻处理一个有效的事件循环有助于减少操作系统上的调度负载。用户层眼中看到的 Goroutine 中的“block socket”实现了 goroutine-per-connection 简单的网络编程模式。实际上是通过 Go runtime 中的 netpoller 通过 Non-block socket I/O 多路复用机制“模拟”出来的。当调用一些系统方法的时候如文件 I/O如果系统方法调用的时候发生阻塞这种情况下网络轮询器NetPoller无法使用而进行系统调用的 G1 将阻塞当前 M1。调度器引入 其它M 来服务 M1 的P。如果在 Goroutine 去执行一个 sleep 操作导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon它监控那些长时间运行的 G 任务然后设置可以强占的标识符别的 Goroutine 就可以抢先进来执行。
具体的调度策略
Go的调度器内部有三个重要的结构G(代表一个goroutine它有自己的栈)M(Machine,代表内核级线程)PProcessor([prɑːsesər])上下文处理器它的主要用途就是用来连接执行的goroutine和内核线程的,定义在源码的src/runtime/runtime.h文件中 -G代表一个goroutine对象每次go调用的时候都会创建一个G对象 -M代表一个线程每次创建一个M的时候都会有一个底层线程创建所有的G任务最终还是在M上执行 -P代表一个处理器每一个运行的M都必须绑定一个P就像线程必须在每一个CPU核上执行一样 一个M对应一个P一个P下面挂多个G但同一时间只有一个G在跑其余都是放入等待队列(runqueue([kjuː]))。 当一个P的队列消费完了就去全局队列里取如果全局队列里也消费完了会去其他P的队列里抢任务所以需要单独存储下一个 g 的地址而不是从队列里获取。
同时启动一万个G如何调度
首先一万个G会按照P的设定个数尽量平均地分配到每个P的本地队列中。如果所有本地队列都满了那么剩余的G则会分配到GMP的全局队列上。接下来便开始执行GMP模型的调度策略
本地队列轮转每个P维护着一个包含G的队列不考虑G进入系统调用或IO操作的情况下P周期性的将G调度到M中执行执行一小段时间将上下文保存下来然后将G放到队列尾部然后从队首中重新取出一个G进行调度。系统调用上面说到P的个数默认等于CPU核数每个M必须持有一个P才可以执行G一般情况下M的个数会略大于P的个数这多出来的M将会在G产生系统调用时发挥作用。当该G即将进入系统调用时对应的M由于陷入系统调用而进被阻塞将释放P进而某个空闲的M1获取P继续执行P队列中剩下的G。工作量窃取多个P中维护的G队列有可能是不均衡的当某个P已经将G全部执行完然后去查询全局队列全局队列中也没有新的G而另一个M中队列中还有3很多G待运行。此时空闲的P会将其他P中的G偷取一部分过来一般每次偷取一半。
抢占式调度及goroutine泄漏
go的抢占式调度
在1.1 版本中的调度器是不支持抢占式调度的程序只能依靠 Goroutine 主动让出 CPU 资源才能触发调度。Go 语言的调度器在 1.2 版本中引入基于协作的抢占式调度解决了以下的问题
某些 Goroutine 可以长时间占用线程造成其它 Goroutine 的饥饿垃圾回收需要暂停整个程序Stop-the-worldSTW最长可能需要几分钟的时间导致整个程序无法工作
1.2 版本的抢占式调度虽然能够缓解这个问题但是它实现的抢占式调度是基于协作的在之后很长的一段时间里 Go 语言的调度器都有一些无法被抢占的边缘情况例如for 循环或者垃圾回收长时间占用线程这些问题中的一部分直到 1.14 才被基于信号的抢占式调度解决。 抢占式分为两种
协作式的抢占式调度基于信号的抢占式调度
Goroutine 泄露
Goroutine 作为一种逻辑上理解的轻量级线程需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息而这些内存在目前版本的 Go 中是不会被释放的。因此如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存就会造成内存泄漏的现象。造成泄露的大多数原因有以下三种
Goroutine 内正在进行 channel/mutex 等读写操作但由于逻辑问题某些情况下会被一直阻塞。Goroutine 内的业务逻辑进入死循环资源一直无法释放。Goroutine 内的业务逻辑进入长时间等待有不断新增的 Goroutine 进入等待。
P和M的数量一定是11吗如果一个G阻塞了会怎么样
不一定M必须持有P才可以执行代码跟系统中的其他线程一样M也会被系统调用阻塞。P的个数在启动程序时决定默认情况下等于CPU的核数可以使用环境变量GOMAXPROCS或在程序中使用runtime.GOMAXPROCS()方法指定P的个数。 M的个数通常稍大于P的个数因为除了运行Go代码runtime包还有其他内置任务需要处理。
一个协程挂起换入另外一个协程是什么过程
对于进程、线程都是有内核进行调度有CPU时间片的概念进行抢占式调度。协程又称微线程纤程。英文名Coroutine。协程的调用有点类似子程序如程序A调用了子程序B子程序B调用了子程序C当子程序C结束了返回子程序B继续执行之后的逻辑当子程序B运行结束了返回程序A直到程序A运行结束。但是和子程序相比协程有挂起的概念协程可以挂起跳转执行其他协程合适的时机再跳转回来。 本质上goroutine就是协程但是完全运行在用户态采用了MPG模型
M内核级线程
G代表一个goroutine
PProcessor处理器用来管理和执行goroutine的。
G-M-P三者的关系与特点
P的个数取决于设置的GOMAXPROCSgo新版本默认使用最大内核数比如你有8核处理器那么P的数量就是8M的数量和P不一定匹配可以设置很多MM和P绑定后才可运行多余的M处于休眠状态。P包含一个LRQLocal Run Queue本地运行队列这里面保存着P需要执行的协程G的队列除了每个P自身保存的G的队列外调度器还拥有一个全局的G队列GRQGlobal Run Queue这个队列存储的是所有未分配的协程G。
golang gmp模型全局队列中的G会不会饥饿,为什么P的数量是多少能修改吗M的数量是多少
第一版
全局队列中的G不会饥饿。 因为线程想运行任务就得获取P从P的本地队列获取GP队列为空时M也会尝试从全局队列拿一批G放到P的本地队列或从其他P的本地队列偷一半放到自己P的本地队列。 M运行GG执行之后M会从P获取下一个G不断重复下去。所以全局队列中的G总是能被消费掉.P的数量可以理解为最大为本机可执行的cpu的最大数量。 通过runtime.GOMAXPROCS(runtime.NumCPU())设置。 runtime.NumCPU()方法返回当前进程可用的逻辑cpu数量。
第二版
全局队列中的G不会饥饿P中每执行61次调度就需要优先从全局队列中获取一个G到当前P中并执行下一个要执行的G。
P数量问题可以通过 runtime.GOMAXPROCS() 设置数量默认为当前CPU可执行的最大数量。M数量问题 Go语⾔本身是限定M的最⼤量是10000。 runtime/debug包中的SetMaxThreads函数来设置。 有⼀个M阻塞会创建⼀个新的M。 如果有M空闲那么就会回收或者睡眠。
内存管理
make和new的异同点
用途不同make 用于创建和初始化引用类型如 slice、map 和 channel而 new 用于创建指针类型的值。返回类型不同make 返回的是所创建类型的引用而 new 返回的是对应类型的指针。参数不同make 接收的参数是类型和一些可选的长度或容量等参数具体取决于所创建的类型。而 new 只接收一个参数即所要创建类型的指针。初始化不同make 创建的引用类型会进行初始化并返回一个可用的、已分配内存的对象。而 new 创建的指针类型只是返回一个对应类型的指针并不会进行初始化。
内存模型
Go语言运行时依靠细微的对象切割、极致的多级缓存、精准的位图管理实现了对内存的精细化管理。 将对象分为微小对象、小对象、大对象使用三级管理结构mcache、mcentral、mheap用于管理、缓存加速span对象的访问和分配使用精准的位图管理已分配的和未分配的对象及对象的大小。 Go语言运行时依靠细微的对象切割、极致的多级缓存、精准的位图管理实现了对内存的精细化管理以及快速的内存访问同时减少了内存的碎片。
span
Go 将内存分成了67个级别的span特殊的0级特殊大对象大小是不固定的。当具体的对象需要分配内存时并不是直接分配span而是分配不同级别的span中的元素。因此span的级别不是以每个span的大小为依据而是以span中元素的大小为依据的。
Span等级元素大小(字节)Span大小(字节)元素个数188192102421681925123328192256448819217065648192128…………65286725734426632768327681
第1级span拥有的元素个数为8192/81024。每个span的大小和span中元素的个数都不是固定的例如第65级span的大小为57344字节每个元素的大小为28672字节元素个数为2。span的大小虽然不固定但其是8KB或更大的连续内存区域。 每个具体的对象在分配时都需要对齐到指定的大小假如我们分配17字节的对象会对应分配到比17字节大并最接近它的元素级别即第3级这导致最终分配了32字节。因此这种分配方式会不可避免地带来内存的浪费。
三级对象管理
为了方便对Span进行管理加速Span对象访问、分配。分别为mcache、mcentral、mheap。 TCMalloc内存分配算法的思想: 每个逻辑处理器P都存储了一个本地span缓存称作mcache。如果协程需要内存可以直接从mcache中获取由于在同一时间只有一个协程运行在逻辑处理器P上所以中间不需要加锁。mcache包含所有大小规格的mspan但是每种规格大小只包含一个。除class0外mcache的span都来自mcentral。
mcentral 所有逻辑处理器P共享的。 对象收集所有给定规格大小的span。每个mcentral都包含两个mspan的链表empty mspanList表示没有空闲对象或span已经被mcache缓存的span链表nonempty mspanList表示有空闲对象的span链表。(为了的分配Mspan到Mcache中) mheap 每个级别的span都会有一个mcentral用于管理span链表0级除外其实 都是一个数组由Mheap管理 作用 不只是管理central大对象也会直接通过mheap进行分配。 mheap实现了对虚拟内存线性地址空间的精准管理建立了span与具体线性地址空间的联系保存了分配的位图信息是管理内存的最核心单元。堆区的内存被分成了HeapArea大小进行管理。对Heap进行的操作必须全局加锁而mcache、mcentral可以被看作某种形式的缓存。
四级内存块管理
Go 根据对象大小将堆内存分成了 HeapArea-chunk-span-page 4种内存块进行管理。不同的内存块用于不同的场景便于高效地对内存进行管理。
HeapArea 内存块最大其大小与平台相关在UNIX 64位操作系统中占据64MB。chunk占据了512KBspan根据级别大小的不同而不同但必须是page的倍数而1个page占据8KB
内存分配的实现
Golang内存分配和TCMalloc差不多都是把内存提前划分成不同大小的块其核心思想是把内存分为多级管理从而降低锁的粒度。先了解下内存管理每一级的概念 mspan mspan跟tcmalloc中的span相似它是golang内存管理中的基本单位也是由页组成的每个页大小为8KB与tcmalloc中span组成的默认基本内存单位页大小相同。mspan里面按照8*2n大小(8b16b32b … )每一个mspan又分为多个object。
mcache mcache跟tcmalloc中的ThreadCache相似ThreadCache为每个线程的cache同理mcache可以为golang中每个Processor提供内存cache使用每一个mcache的组成单位也是mspan。
mcentral mcentral跟tcmalloc中的CentralCache相似当mcache中空间不够用可以向mcentral申请内存。可以理解为mcentral为mcache的一个“缓存库”供mcaceh使用。它的内存组成单位也是mspan。mcentral里有两个双向链表一个链表表示还有空闲的mspan待分配一个表示链表里的mspan都被分配了。
mheap mheap跟tcmalloc中的PageHeap相似负责大内存的分配。当mcentral内存不够时可以向mheap申请。那mheap没有内存资源呢?跟tcmalloc一样向OS操作系统申请。还有大于32KB的内存也是直接向mheap申请。
golang 分配内存具体过程如下
程序启动时申请一大块内存并划分成spans、bitmap、arena区域arena区域按页划分成一个个小块span管理一个或多个页mcentral管理多个span供线程申请使用mcache作为线程私有资源资源来源于mcentral
简单介绍一下go的内存分配机制有mcentral为啥要mcache
第一版
1.介绍内存分配机制
GO语言内存管理子系统主要由两部分组成内存分配器和垃圾回收器gc。内存分配器主要解决小对象的分配管理和多线程的内存分配问题。什么是小对象呢小于等于32k的对象就是小对象其它都是大对象。小对象的内存分配是通过一级一级的缓存来实现的目的就是为了提升内存分配释放的速度以及避免内存碎片等问题
2.介绍MCentral
所有线程共享的组件不是独占的因此需要加锁操作。它其实也是一个缓存cache的一个上游用户但缓存的不是小对象内存块而是一组一组的内存page一个page4K。从图2可以看出在heap结构里使用了一个0到n的数组来存储了一批central并不是只有一个central对象。从上面结构定义可以知道这个数组长度位61个元素也就是说heap里其实是维护了61个central这61个central对应了cache中的list数组也就是每一个sizeclass就有一个central。所以在cache中申请内存时如果在某个sizeclass的内存链表上找不到空闲内存那么cache就会向对应的sizeclass的central获取一批内存块。注意这里central数组的定义里面使用填充字节这是因为多线程会并发访问不同central避免false sharing。
3.介绍mcache
每个线程都有一个cache用来存放小对象。由于每个线程都有cache所以获取空闲内存是不用加锁的。cache层的主要目的就是提高小内存的频繁分配释放速度。 我们在写程序的时候其实绝大多数的内存申请都是小于32k的属于小对象因此这样的内存分配全部走本地cache不用向操作系统申请显然是非常高效的
4.阐述二者区别
mcentral与mcache有一个明显区别就是有锁存在由于mcentral是公共资源会有多个mcache向它申请mspan因此必须加锁另外mcentral与mcache不同由于P绑定了很多Goroutine在P上会处理不同大小的对象mcache就需要包含各种规格的mspan但mcentral不同同一个mcentral只负责一种规格的mspan就够了。
第二版
Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法其核心思想是内存池 多级对象管理。内存池主要是预先分配内存减少向系统申请的频率多级对象有mheap、mspan、arenas、mcentral、mcache。它们以 mspan 作为基本分配单位。具体的分配逻辑如下 当要分配大于 32K 的对象时从 mheap 分配。 当要分配的对象小于等于 32K 大于 16B 时从 P 上的 mcache 分配如果 mcache 没有内存则从 mcentral 获取如果 mcentral 也没有则向 mheap 申请如果 mheap 也没有则从操作系统申请内存。 当要分配的对象小于等于 16B 时从 mcache 上的微型分配器上分配。
GC触发时机
内存分配量达到阀值触发GC
每次内存分配时都会检查当前内存分配量是否已达到阀值如果达到阀值则立即启动GC。
阀值 上次GC内存分配量 * 内存增长率内存增长率由环境变量GOGC控制默认为100即每当内存扩大一倍时启动GC
定期触发GC
默认情况下最长2分钟触发一次GC这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明
var forcegcperiod int64 2 * 60 * 1e9手动触发
程序代码中也可以使用runtime.GC()来手动触发GC。这主要用于GC性能测试和统计。
go垃圾回收介绍
第一版
三色标记法混合写屏障
初始状态下所有对象都是白色的。从根节点开始遍历所有对象把遍历到的对象变成灰色对象遍历灰色对象将灰色对象引用的对象也变成灰色对象然后将遍历过的灰色对象变成黑色对象。循环步骤3直到灰色对象全部变黑色。通过写屏障(write-barrier)检测对象有变化重复以上操作收集所有白色对象垃圾。 标记清除: 此算法主要有两个主要的步骤 标记(Mark phase) 清除(Sweep phase) 第一步找出不可达的对象然后做上标记。 第二步回收标记好的对象。 操作非常简单但是有一点需要额外注意mark and sweep算法在执行的时候需要程序暂停即 stop the world。 也就是说这段时间程序会卡在哪儿。故中文翻译成 卡顿. 标记-清扫(Mark And Sweep)算法存在什么问题 标记-清扫(Mark And Sweep)算法这种算法虽然非常的简单但是还存在一些问题 STWstop the world让程序暂停程序出现卡顿。 标记需要扫描整个heap 清除数据会产生heap碎片 这里面最重要的问题就是mark-and-sweep 算法会暂停整个程序。 三色并发标记法: 首先程序创建的对象都标记为白色。 gc开始扫描所有可到达的对象标记为灰色 从灰色对象中找到其引用对象标记为灰色把灰色对象本身标记为黑色 监视对象中的内存修改并持续上一步的操作直到灰色标记的对象不存在 此时gc回收白色对象 最后将所有黑色对象变为白色并重复以上所有过程。 混合写屏障: 注意 当gc进行中时新创建一个对象. 按照三色标记法的步骤,对象会被标记为白色,这样新生成的对象最后会被清除掉这样会影响程序逻辑. golang引入写屏障机制.可以监控对象的内存修改并对对象进行重新标记. gc一旦开始无论是创建对象还是对象的引用改变都会先变为灰色。
第二版
goalng1.8的GC采用三色标记法混合写屏障
三色标记法将所有对象分为三类白色、黑色与灰色。
白色暂无对象引用的潜在垃圾其内存可能会被垃圾回收器回收
黑色表示活跃的对象
灰色黑色与白色的中间状态
三色标记算法分五步进行。
将所有的对象标记为白色从根节点出发将第一次遍历到的节点标记为灰色遍历节点将灰色节点遍历到的白色节点标记为灰色把遍历到的灰色节点标记为黑色循环执行该过程直到没有灰色节点回收所有白色节点
屏障机制分为插入屏障和删除屏障插入屏障实现的是强三色不变式删除屏障则实现了弱三色不变式。值得注意的是为了保证栈的运行效率屏障只对堆上的内存对象启用栈上的内存会在GC结束后启用STW重新扫描。
插入屏障对象被引用时触发的机制当白色对象被黑色对象引用时白色对象被标记为灰色栈上对象无插入屏障。
C语言这种较为传统的语言通过malloc和free手动向操作系统申请和释放内存这种自由管理内存的方式给予程序员极大的自由度但是也相应地提高了对程序员的要求。C语言的内存分配和回收方式主要包括三种
函数体内的局部变量在栈上创建函数作用域结束后自动释放内存静态变量在静态存储区域上分配内存整个程序运行结束后释放全局生命周期动态分配内存的变量在堆上分配通过malloc申请free释放
C、C和Rust等较早的语言采用的是手动垃圾回收需要程序员通过向操作系统申请和释放内存来手动管理内存程序员极容易忘记释放自己申请的内存对于一个长期运行的程序往往是一个致命的缺点。
Java垃圾回收
就是将 对象的内存周期划分为几块按照每块的情况采取不同的垃圾回收算法。一般是把Java堆分为新生代和老年代。年轻代:年轻代用来存放新近创建的对象,年轻代中存在的对象是死亡非常快的。存在朝生夕死的情况。 老年代:老年代中存放的对象是存活了很久的对象。 垃圾回收算法分为三种分别为标记-清除算法复制算法标记-整理算法。
标记-清除算法:标记无用对象然后对其进行清除回收。 复制算法将内存区域划分为大小相等的两部分每次只使用一部分当该部分用完后将其存活的对象移至另一部分,并把该部分内存全部清除。 标记-整理算法标记无用对象,让所有存活的对象都向内存一端移动然后清除掉存活对象边界外的内存区域。
golang逃逸分析
Golang 的逃逸分析是指编译器根据代码的特征和生命周期自动的把变量分配到堆或者是栈上面。Go 在编译阶段确立逃逸并不是在运行时。可以使用 -gcflags-m 参数来查看逃逸分析的详细信息包括哪些变量逃逸到堆上。
介绍栈堆
栈 stack是系统自动分配空间的例如我们定义一个 char a系统会自动在栈上为其开辟空间。而堆heap则是程序员根据需要自己申请的空间例如 malloc10开辟十个字节的空间。栈在内存中是从高地址向下分配的并且连续的遵循先进后出原则。系统在分配的时候已经确定好了栈的大小空间。栈上面的空间是自动回收的所以栈上面的数据的生命周期在函数结束后就被释放掉了。堆分配是从低地址向高地址分配的每次分配的内存大小可能不一致导致了空间是不连续的这也产生内存碎片的原因。由于是程序分配所以效率相对慢些。而堆上的数据只要程序员不释放空间就一直可以访问到不过缺点是一旦忘记释放会造成内存泄露。
逃逸策略
每当函数中申请新的对象编译器会根据该对象是否被函数外部引用来决定是否逃逸
如果函数外部没有引用则优先放到栈中如果函数外部存在引用则必定放到堆中
注意对于函数外部没有引用的对象也有可能放到堆中比如内存过大超过栈的存储能力。
逃逸分析好处
内存分配优化逃逸分析可以帮助编译器确定哪些变量可以在栈上分配而不是在堆上分配。栈上分配的变量生命周期受限于函数或栈帧的范围分配和释放内存的开销较小可以提高程序的性能。减少内存压力通过将变量分配在栈上可以减少对堆的内存压力。这对于大量临时对象的创建和销毁非常有用可以减少垃圾回收的频率提高程序的吞吐量。减少垃圾回收压力逃逸分析可以减少不必要的堆分配从而减少垃圾回收器的负担。这对于大型和长时间运行的应用程序尤为重要可以降低垃圾回收的停顿时间。
常见的逃逸现象
func函数类型数据类型interface{} 数据类型 指针类型
[]interface{}数据类型通过[]赋值必定会出现逃逸。map[string]interface{}类型尝试通过赋值必定会出现逃逸。map[interface{}]interface{}类型尝试通过赋值会导致key和value的赋值出现逃逸。map[string][]string数据类型赋值会发生[]string发生逃逸。[]*int数据类型赋值的右值会发生逃逸现象。func(*int)函数类型进行函数赋值会使传递的形参出现逃逸现象。func([]string): 函数类型进行[]string{value}赋值会使传递的参数出现逃逸现象。chan []string数据类型想当前channel中传输[]string{value}会发生逃逸现象。发送指针或带有指针的值到channel因为编译时候无法知道那个goroutine会在channel接受数据编译器无法知道什么时候释放。在一个切片上存储指针或带指针的值。比如[]*string导致切片内容逃逸其引用值一直在堆上。切片的append导致超出容量切片重新分配地址切片背后的存储基于运行时的数据进行扩充就会在堆上分配。调用接口类型时接口类型的方法调用是动态调度实际使用的具体实现只能在运行时确定如一个接口类型为io.Reader的变量r对r.Read(b)的调用将导致r的值和字节片b的后续转义并因此分配到堆上。在方法内把局部变量指针返回被外部引用其生命周期大于栈导致内存溢出。
避免逃逸方法
不要盲目使用变量指针作为参数虽然减少了复制但变量逃逸的开销更大。预先设定好slice长度避免频繁超出容量重新分配。一个经验是指针指向的数据大部分在堆上分配的。
写代码时如何减少对象分配
例如如果需要把数字转换成字符串使用 strconv.Itoa() 比 fmt.Sprintf() 要快一倍左右。如果需要把数字转换成字符串使用 strconv.Itoa() 比 fmt.Sprintf() 要快一倍左右。
内存分配和tcmalloc的区别
go 内存分配核心思想就是把内存分为多级管理从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理每个线程都会自行维护一个独立的内存池进行内存分配时优先从该内存池中分配当内存池不足时才会向全局内存池申请以避免不同线程对全局内存池的频繁竞争。
Go在程序启动时会向操作系统申请一大块内存之后自行管理。Go内存管理的基本单元是mspan它由若干个页组成每种mspan可以分配特定大小的object。mcache, mcentral, mheap是Go内存管理的三大组件层层递进。mcache管理线程在本地缓存的mspanmcentral管理全局的mspan供所有线程使用mheap管理Go的所有动态分配内存。极小的对象(16B)会分配在一个object中以节省资源使用tiny分配器分配内存一般对象(16B-32KB)通过mspan分配内存大对象(32KB)则直接由mheap分配内存。
tcmalloc tcmalloc 是google开发的内存分配算法库最开始它是作为google的一个性能工具库 perftools 的一部分。TCMalloc是用来替代传统的malloc内存分配函数。它有减少内存碎片适用于多核更好的并行性支持等特性。 TC就是Thread Cache两英文的简写。它提供了很多优化如 1.TCMalloc用固定大小的page(页)来执行内存获取、分配等操作。这个特性跟Linux物理内存页的划分是不是有同样的道理。 2.TCMalloc用固定大小的对象比如8KB16KB 等用于特定大小对象的内存分配这对于内存获取或释放等操作都带来了简化的作用。 3.TCMalloc还利用缓存常用对象来提高获取内存的速度。 4.TCMalloc还可以基于每个线程或者每个CPU来设置缓存大小这是默认设置。 5.TCMalloc基于每个线程独立设置缓存分配策略减少了多线程之间锁的竞争。
Go中的内存分类并不像TCMalloc那样分成小、中、大对象但是它的小对象里又细分了一个Tiny对象Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象。小对象和大对象只用大小划定无其他区分。 Go内存管理与tcmalloc最大的不同在于它提供了逃逸分析和垃圾回收机制。
Go 语言内存分配什么分配在堆上什么分配在栈上
Go 语言有两部分内存空间栈内存和堆内存。栈内存由编译器自动分配和释放函数调用的参数、返回值以及局部变量大都会被分配到栈上。堆内存的生命周期比栈内存要长如果函数返回的值还会在其他地方使用那么这个值就会被编译器自动分配到堆上。堆内存相比栈内存来说不能自动被编译器释放只能通过垃圾回收器才能释放所以栈内存效率会很高。
go性能调优的方法
内存优化
将小对象合并成结构体一次分配减少内存分配次数 Go runtime底层采用内存池机制每个span大小为4k同时维护一个cache。cache有一个0到n的list数组list数组的每个单元挂载的是一个链表链表的每个节点就是一块可用的内存块同一链表中的所有节点内存块都是大小相等的但是不同链表的内存大小是不等的即list数组的一个单元存储的是一类固定大小的内存块不同单元里存储的内存块大小是不等的。cache缓存的是不同类大小的内存对象申请的内存大小最接近于哪类缓存内存块时就分配哪类内存块。当cache不够时再向spanalloc中分配。缓存区内容一次分配足够大小空间并适当复用 在协议编解码时需要频繁地操作[]byte可以使用bytes.Buffer或其它byte缓存区对象。 bytes.Buffer等通过预先分配足够大的内存避免当增长时动态申请内存减少内存分配次数。对于byte缓存区对象需要考虑适当地复用。slice和map采make创建时预估大小指定容量 slice和map与数组不一样不存在固定空间大小可以根据增加元素来动态扩容。 slice初始会指定一个数组当对slice进行append等操作时当容量不够时会自动扩容 如果新的大小是当前大小2倍以上则容量增涨为新的大小 否则循环以下操作如果当前容量小于1024按2倍增加否则每次按当前容量1/4增涨直到增涨的容量超过或等新大小。 map的扩容比较复杂每次扩容会增加到上次容量的2倍。map的结构体中有一个buckets和oldbuckets用于实现增量扩容 正常情况下直接使用bucketsoldbuckets为空 如果正在扩容则oldbuckets不为空buckets是oldbuckets的2倍 因此建议初始化时预估大小指定容量长调用栈避免申请较多的临时对象 Goroutine的调用栈默认大小是4K1.7修改为2K采用连续栈机制当栈空间不够时Go runtime会自动扩容 当栈空间不够时按2倍增加原有栈的变量会直接copy到新的栈空间变量指针指向新的空间地址 退栈会释放栈空间的占用GC时发现栈空间占用不到1/4时则栈空间减少一半。 比如栈的最终大小2M则极端情况下就会有10次的扩栈操作会带来性能下降。 因此建议控制调用栈和函数的复杂度不要在一个goroutine做完所有逻辑如的确需要长调用栈而考虑goroutine池化避免频繁创建goroutine带来栈空间的变化。避免频繁创建临时对象 Go在GC时会引发stop the world即整个情况暂停。Go1.8最坏情况下GC为100us。但暂停时间还是取决于临时对象的个数临时对象数量越多暂停时间可能越长并消耗CPU。 因此建议GC优化方式是尽可能地减少临时对象的个数尽量使用局部变量所多个局部变量合并一个大的结构体或数组减少扫描对象的次数一次回尽可能多的内存。
并发优化
高并发的任务处理使用goroutine池 Goroutine虽然轻量但对于高并发的轻量任务处理频繁来创建goroutine来执行执行效率并不会太高因为过多的goroutine创建会影响go runtime对goroutine调度以及GC消耗高并发时若出现调用异常阻塞积压大量的goroutine短时间积压可能导致程序崩溃。避免高并发调用同步系统接口 goroutine的实现是通过同步来模拟异步操作。 网络IO、锁、channel、Time.sleep、基于底层系统异步调用的Syscall操作并不会阻塞go runtime的线程调度。 本地IO调用、基于底层系统同步调用的Syscall、CGo方式调用C语言动态库中的调用IO或其它阻塞会创建新的调度线程。 网络IO可以基于epoll的异步机制或kqueue等异步机制但对于一些系统函数并没有提供异步机制。例如常见的posix api中对文件的操作就是同步操作。虽有开源的fileepoll来模拟异步文件操作。但Go的Syscall还是依赖底层的操作系统的API。系统API没有异步Go也做不了异步化处理。 因此建议把涉及到同步调用的goroutine隔离到可控的goroutine中而不是直接高并的goroutine调用。高并发时避免共享对象互斥 传统多线程编程时当并发冲突在4~8线程时性能可能会出现拐点。Go推荐不通过共享内存来通信Go创建goroutine非常容易当大量goroutine共享同一互斥对象时也会在某一数量的goroutine出在拐点。 因此建议goroutine尽量独立无冲突地执行若goroutine间存在冲突则可以采分区来控制goroutine的并发个数减少同一互斥对象冲突并发数。
其它优化
避免使用CGO或者减少CGO调用次数 GO可以调用C库函数但Go带有垃圾收集器且Go的栈动态增涨无法与C无缝地对接。Go的环境转入C代码执行前必须为C创建一个新的调用栈把栈变量赋值给C调用栈调用结束现拷贝回来。调用开销较大需要维护Go与C的调用上下文两者调用栈的映射。相比直接的GO调用栈单纯的调用栈可能有2个甚至3个数量级以上。 因此建议尽量避免使用CGO无法避免时要减少跨CGO的调用次数。减少[]byte与string之间转换尽量采用[]byte来字符串处理 GO里面的string类型是一个不可变类型GO中[]byte与string底层是两个不同的结构转换存在实实在在的值对象拷贝所以尽量减少不必要的转化。 因此建议存在字符串拼接等处理尽量采用[]byte。字符串的拼接优先考虑bytes.Buffer string类型是一个不可变类型但拼接会创建新的string。GO中字符串拼接常见有如下几种方式 string 操作 导致多次对象的分配与值拷贝 fmt.Sprintf 会动态解析参数效率好不哪去 strings.Join 内部是[]byte的append bytes.Buffer 可以预先分配大小减少对象分配与拷贝 因此建议对于高性能要求优先考虑bytes.Buffer预先分配大小。
虚拟内存有什么作用 无效属于操作系统
虚拟内存就是说让物理内存扩充成更⼤的逻辑内存从⽽让程序获得更多的可⽤内存。虚拟内存使⽤部分加载的 技术让⼀个进程或者资源的某些⻚⾯加载进内存从⽽能够加载更多的进程甚⾄能加载⽐内存⼤的进程这样 看起来好像内存变⼤了这部分内存其实包含了磁盘或者硬盘并且就叫做虚拟内存。
并发编程
说一下reflect
recflect是golang用来检测存储在接口变量内部(值value类型concrete type) pair对的一种机制。它提供了两种类型或者说两个方法让我们可以很容易的访问接口变量内容分别是reflect.ValueOf() 和 reflect.TypeOf()。
ValueOf用来获取输入参数接口中的数据的值如果接口为空则返回0TypeOf用来动态获取输入参数接口中的值的类型如果接口为空则返回nil
runtime提供常见的方法
Gosched()让当前线程让出 cpu 以让其它线程运行它不会挂起当前线程因此当前线程未来会继续执行。NumCPU()返回当前系统的 CPU 核数量。GOMAXPROCS()设置最大的可同时使用的 CPU 核数。 通过runtime.GOMAXPROCS函数应用程序可以设置运行时系统中的 P 最大数量。注意如果在运行期间设置该值的话会引起“Stop the World”。所以应在应用程序最早期调用并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS而不是在程序中调用runtime.GOMAXPROCS函数。无论我们传递给函数的整数值是什么值运行时系统的P最大值总会在1~256之间。go1.8 后默认让程序运行在多个核上可以不用设置了。go1.8 前还是要设置一下可以更高效的利用 cpu。Goexit()退出当前 goroutine但是defer语句会照常执行。NumGoroutine返回正在执行和排队的任务总数。 runtime.NumGoroutine函数在被调用后会返回系统中的处于特定状态的 Goroutine 的数量。这里的特定状态是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。注意垃圾回收所在Groutine的状态也处于这个范围内的话也会被纳入该计数器。GOOS查看目标操作系统。很多时候我们会根据平台的不同实现不同的操作就可以用GOOS来查看自己所在的操作系统。runtime.GC会让运行时系统进行一次强制性的垃圾收集。 强制的垃圾回收不管怎样都要进行的垃圾回收。非强制的垃圾回收只会在一定条件下进行的垃圾回收即运行时系统自上次垃圾回收之后新申请的堆内存的单元也成为单元增量达到指定的数值。GOROOT()获取 goroot 目录。runtime.LockOSThread 和 runtime.UnlockOSThread 函数前者调用会使调用他的 Goroutine 与当前运行它的M锁定到一起后者调用会解除这样的锁定。
sync.once 如何实现并发安全
type Once struct {done unit32m Mutex
}他们分别为标记是否已经执行过的标志(done),以及执行时所用的互斥锁(m) 除了结构体外sync.Once还包括了一个公开的方法Do:
func (o *Once) Do(f func()) {if atomic.LoadUint32(o.done) 0 {o.doSlow(f)}
}Once.Do方法的实现非常简单通过atomic.LoadUint32获取Once实例的done属性值。 若done值为0时表示函数f未被调用过或正运行中且未结束则将调用doSlow方法 若done值为1时表示函数f已经调用且完成则直接返回。 这里使用了原子操作方法atomic.LoadUint32而不是直接将o.done进行比较也是为了避免并发状态下错误地判断执行状态产生不必要的锁操作带来的时间开销。
func (o *Once) doSlow(f func()) {o.m.Lock()defer o.m.Unlock()if o.done 0 {defer atomic.StoreUint32(o.done, 1)f()}
}Once.doSlow方法的实现使用了传统的互斥锁Mutex操作在执行时即调用o.m.Lock方法获得锁然后再继续判断是否已经完成并调用f函数。 可以看到在获得锁后还需要对o.done的值进行一次判断避免了f函数被重复调用。 最后在退出doSlow方法时还需要对获取的锁进行释放若进入到f函数的调用则需要更改o.done属性值。
context数据结构
Context 是一个接口定义了 4 个方法它们都是幂等的。也就是说连续多次调用同一个方法得到的结果都是相同的。
Done() 返回一个 channel可以表示 context 被取消的信号当这个 channel 被关闭时说明 context 被取消了。注意这是一个只读的channel。 我们又知道读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说这是一个 receive-only 的 channel。因此在子协程里读这个 channel除非被关闭否则读不出来任何东西。也正是利用了这一点子协程从 channel 里读出了值零值后就可以做一些收尾工作尽快退出。Err() 返回一个错误表示 channel 被关闭的原因。例如是被取消还是超时。Deadline() 返回 context 的截止时间通过此时间函数就可以决定是否进行接下来的操作如果时间太短就可以不往下做了否则浪费系统资源。当然也可以用这个 deadline 来设置一个 I/O 操作的超时时间。Value() 获取之前设置的 key 对应的 value。
go 怎么控制查询timeout context
context 监听是否有 IO 操作开始从当前连接中读取网络请求每当读取到一个请求则会将该cancelCtx传入用以传递取消信号可发送取消信号取消所有进行中的网络请求。
Deadline — 返回 context.Context 被取消的时间也就是完成工作的截止日期Done — 返回一个 Channel这个 Channel 会在当前工作完成或者上下文被取消之后关闭多次调用 Done 方法会返回同一个 ChannelErr — 返回 context.Context 结束的原因它只会在 Done 返回的 Channel 被关闭时才会返回非空的值 如果 context.Context 被取消会返回 Canceled 错误如果 context.Context 超时会返回 DeadlineExceeded 错误 Value — 从 context.Context 中获取键对应的值对于同一个上下文来说多次调用 Value 并传入相同的 Key 会返回相同的结果该方法可以用来传递请求特定的数据
go并发优秀在哪里
Go中天然的支持并发Go允许使用go语句开启一个新的运行期线程即 goroutine以一个不同的、新创建的goroutine来执行一个函数。同一个程序中的所有goroutine共享同一个地址空间。 Goroutine非常轻量除了为之分配的栈空间其所占用的内存空间微乎其微。并且其栈空间在开始时非常小之后随着堆存储空间的按需分配或释放而变化。内部实现上goroutine会在多个操作系统线程上多路复用。如果一个goroutine阻塞了一个操作系统线程例如等待输入这个线程上的其他goroutine就会迁移到其他线程这样能继续运行。开发者并不需要关心/担心这些细节。 Go语言的并发机制运用起来非常简便在启动并发的方式上直接添加了语言级的关键字就可以实现和其他编程语言相比更加轻量。
高并发特点
用户空间避免了内核态和用户态的切换导致的成本可以由语言和框架层进行调度更小的栈空间允许创建大量的实例 2) channel 被单独创建并且可以在进程之间传递它的通信模式类似于 boss-worker 模式的一个实体通过将消息发送到 channel 中然后又监听这个 channel 的实体处理两个实体之间是匿名的这个就实现实体中间的解耦在实现原理上其实是一个阻塞的消息队列。 3) 调度器 goroutine 中提供了调度器在调度器加入了steal working 算法 goroutine 是可以被异步抢占因此没有函数调用的进程不再对调度器造成死锁或造成垃圾回收的大幅变慢。并且 go 对网络IO库进行了封装屏蔽了复杂的细节对外提供统一的语法关键字支持简化了并发程序编写的成本。
golang并发控制
数据安全控制
互斥锁 sync.Mutex读写锁 sync.RWMutex原子操作 sync/atomic
并发行为控制
golang控制并发有三种经典的方式,一种是通过channel通知实现并发控制 一种是WaitGroup,另外一种就是Context。
使用最基本通过channel通知实现并发控制 无缓冲通道: 无缓冲的通道指的是通道的大小为0也就是说这种类型的通道在接收前没有能力保存任何值它要求发送 goroutine 和接收 goroutine 同时准备好才可以完成发送和接收操作。 从上面无缓冲的通道定义来看发送 goroutine 和接收 gouroutine 必须是同步的同时准备后如果没有同时准备好的话先执行的操作就会阻塞等待直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。通过sync包中的WaitGroup实现并发控制 在 sync 包中提供了 WaitGroup 它会等待它收集的所有 goroutine 任务全部完成在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。 在每一个 goroutine 完成后 Done() 表示这一个goroutine 已经完成当所有的 goroutine 都完成后在主 goroutine 中 WaitGroup 返回返回。在Go 1.7 以后引进的强大的Context上下文实现并发控制 在一些简单场景下使用 channel 和 WaitGroup 已经足够了但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。 比如一个网络请求 Request每个 Request 都需要开启一个 goroutine 做一些事情这些 goroutine 又可能会开启其他的 goroutine比如数据库和RPC服务。 所以我们需要一种可以跟踪 goroutine 的方案才可以达到控制他们的目的这就是Go语言为我们提供的 Context称之为上下文非常贴切它就是goroutine 的上下文。 它是包括一个程序的运行环境、现场和快照等。每个程序要运行时都需要知道当前程序的运行状态通常Go 将这些封装在一个 Context 里再将它传给要执行的 goroutine 。 context 包主要是用来处理多个 goroutine 之间共享数据及多个 goroutine 的管理。 context包方法: Done() 返回一个只能接受数据的channel类型当该context关闭或者超时时间到了的时候该channel就会有一个取消信号 Err() 在Done() 之后返回context 取消的原因。 Deadline() 设置该context cancel的时间点 Value() 方法允许 Context 对象携带request作用域的数据该数据必须是线程安全的。
golang支持哪些并发机制
Go语言中实现了两种并发模型一种是我们熟悉的线程与锁的并发模型它主要依赖于共享内存实现的。程序的正确运行很大程度依赖于开发人员的能力和技巧程序在出错时不易排查。另一种就是CSP并发模型它使用通信的手段来共享内存。CSP中的并发实体是独立的它们之间没有共享的内存空间它们之间的数据交换通过通道实现的
CSP并发模型
Go实现了两种并发模式。第一种多线程共享内存。第二种通过通信来共享内存CSP
CSP并发模型是Go语言特有的并发模型也是Go语言官方所推荐的并发模型。
Go的CSP并发模型是由Go语言中的goroutine与channel共同来实现的。
goroutineGo语言中使用关键字go来创建goroutine。将关键字go放到需要调用的函数前在相同地址空间调用运行这个函数该函数在执行的时候会创建一个独立的线程去执行这个线程就是Go语言中的goroutine。channelGo语言中goroutine之间的通信机制
线程模型 一对一模型1:1 将一个用户级线程映射到一个内核线程每一个线程由内核调度器独立调度线程之间互不影响 优点在多核处理器的条件下实现了真正的并行。 缺点为每一个用户级线程建立一个内核线程开销大浪费资源。 多对一模型M:1 将多个用户级线程映射到一个内核线程。 优点线程上下文切换发生在用户空间。 缺点只有一个处理器被应用在多处理环境下是不可以被接受的实现了并发不能解决并行问题。 多对多模型M:N 多个用户级线程运行在多个内核线程上这使得大部分的线程上下文切换都发生在用户空间而多个内核线程又能充分利用处理器资源
golang中Context的使用场景
Go1.7加入到标准库在于控制goroutine的生命周期。当一个计算任务被goroutine承接了之后由于某种原因我们希望中止这个goroutine的计算任务那么就用得到这个Context了。 包含CancelContext,TimeoutContext,DeadLineContext,ValueContext
场景一RPC调用 在主goroutine上有4个RPCRPC2/3/4是并行请求的我们这里希望在RPC2请求失败之后直接返回错误并且让RPC3/4停止继续计算。这个时候就使用的到Context。
场景二PipeLine runSimplePipeline的流水线工人有三个lineListSource负责将参数一个个分割进行传输lineParser负责将字符串处理成int64,sink根据具体的值判断这个数据是否可用。他们所有的返回值基本上都有两个chan一个用于传递数据一个用于传递错误。-chan string, -chan error输入基本上也都有两个值一个是Context用于传声控制的一个是(in -chan)输入产品的。
场景三超时请求 我们发送RPC请求的时候往往希望对这个请求进行一个超时的限制。当一个RPC请求超过10s的请求自动断开。当然我们使用CancelContext也能实现这个功能开启一个新的goroutine这个goroutine拿着cancel函数当时间到了就调用cancel函数。鉴于这个需求是非常常见的context包也实现了这个需求timerCtx。具体实例化的方法是 WithDeadline 和 WithTimeout。具体的timerCtx里面的逻辑也就是通过time.AfterFunc来调用ctx.cancel的。
场景四HTTP服务器的request互相传递数据 context还提供了valueCtx的数据结构。这个valueCtx最经常使用的场景就是在一个http服务器中在request中传递一个特定值比如有一个中间件做cookie验证然后把验证后的用户名存放在request中。我们可以看到官方的request里面是包含了Context的并且提供了WithContext的方法进行context的替换。
用共享内存的方式实现并发如何保证安全
Go的设计思想就是, 不要通过共享内存来通信而是通过通信来共享内存前者就是传统的加锁后者就是Channel。也就是说设计Channel的主要目 的就是在多任务间传递数据的本身就是安全的。 看源码就知道channel内部维护了一个互斥锁来保证线程安全,channel底层实现出队入队时也加锁。
从运行速度来讲go的并发模型channel和goroutine
goroutine 是一种非常轻量级的实现可在单个进程里执行成千上万的并发任务它是Go语言并发设计的核心。 说到底 goroutine 其实就是线程但是它比线程更小十几个 goroutine 可能体现在底层就是五六个线程而且Go语言内部也实现了 goroutine 之间的内存共享。 使用 go 关键字就可以创建 goroutine将 go 声明放到一个需调用的函数之前在相同地址空间调用运行这个函数这样该函数执行时便会作为一个独立的并发线程这种线程在Go语言中则被称为 goroutine。channel 是Go语言在语言级别提供的 goroutine 间的通信方式。可以使用 channel 在两个或多个 goroutine 之间传递消息
怎么理解“不要用共享内存来通信而是用通信来共享内存”
共享内存会涉及到多个线程同时访问修改数据的情况为了保证数据的安全性那就会加锁加锁会让并行变为串行cpu此时也会忙于线程抢锁。另外使用过多的锁容易使得程序的代码逻辑坚涩难懂并且容易使程序死锁死锁了以后排查问题相当困难特别是很多锁同时存在的时候。
在这种情况下不如换一种方式把数据复制一份每个线程有自己的只要一个线程干完一件事其他线程不用去抢锁了这就是一种通信方式把共享的以通知方式交给线程实现并发。go语言的channel就保证同一个时间只有一个goroutine能够访问里面的数据为开发者提供了一种优雅简单的工具所以go原生的做法就是使用channle来通信而不是使用共享内存来通信。