如果该函数会修改receiver,此时一定要用指针
成都创新互联公司长期为成百上千家客户提供的网站建设服务,团队从业经验10年,关注不同地域、不同群体,并针对不同对象提供差异化的产品和服务;打造开放共赢平台,与合作伙伴共同营造健康的互联网生态环境。为潼南企业提供专业的网站设计制作、做网站,潼南网站改版等技术服务。拥有十多年丰富建站经验和众多成功案例,为您定制开发。
如果receiver是 struct 并且包含互斥类型 sync.Mutex ,或者是类似的同步变量,receiver必须是指针,这样可以避免对象拷贝
如果receiver是较大的 struct 或者 array ,使用指针则更加高效。多大才算大?假设struct内所有成员都要作为函数变量传进去,如果觉得这时数据太多,就是struct太大
如果receiver是 struct , array 或者 slice ,并且其中某个element指向了某个可变量,则这个时候receiver选指针会使代码的意图更加明显
如果receiver使较小的 struct 或者 array ,并且其变量都是些不变量、常量,例如 time.Time ,value receiver更加适合,因为value receiver可以减少需要回收的垃圾量。
golang是一门自带垃圾回收的语言,它的内存分配器和tmalloc(thread-caching malloc)很像,大多数情况下是不需要用户自己管理内存的。最近了解了一下golang内存管理,写出来分享一下,不正确的地方请大佬们指出。
1.内存池:
应该有一个主要管理内存分配的部分,向系统申请大块内存,然后进行管理和分配。
2.垃圾回收:
当分配的内存使用完之后,不直接归还给系统,而是归还给内存池,方便进行下一次复用。至于垃圾回收选择标记回收,还是分代回收算法应该符合语言设计初衷吧。
3.大小切分:
使用单独的数组或者链表,把需要申请的内存大小向上取整,直接从这个数组或链表拿出对应的大小内存块,方便分配内存。大的对象以页申请内存,小的对象以块来申请,避免内存碎片,提高内存使用率。
4.多线程管理:
每个线程应该有自己的内存块,这样避免同时访问共享区的时候加锁,提升语言的并发性,线程之间通信使用消息队列的形式,一定不要使用共享内存的方式。提供全局性的分配链,如果线程内存不够用了,可向分配链申请内存。
这样的内存分配设计涵盖了大部分语言的,上面的想法其实是把golang语言内存分配抽象出来。其实Java语言也可以以同样的方式理解。内存池就是JVM堆,主要负责申请大块内存;多线程管理方面是使用栈内存,每个线程有自己独立的栈内存进行管理。
golang内存分配器
golang内存分配器主要包含三个数据结构:MHeap,MCentral以及MCache
1.MHeap:分配堆,主要是负责向系统申请大块的内存,为下层MCentral和MCache提供内存服务。他管理的基本单位是MSpan(若干连续内存页的数据结构)
type MSpan struct
{
MSpan *next;
MSpan *prev;
PageId start; // 开始的页号
uintptr npages; // 页数
…..
};
可以看出MSpan是一个双端链表的形式,里面存储了它的一些位置信息。
通过一个基地址+(页号*页大小),就可以定位到这个MSpan的实际内存空间。
type MHeap struct
{
lock mutex;
free [_MaxMHeapList] mSpanList // free lists of given length
freelarge mSpanList // free lists length = _MaxMHeapList
busy [_MaxMHeapList] mSpanList // busy lists of large objects of given length
busylarge mSpanList
};
free数组以span为序号管理多个链表。当central需要时,只需从free找到页数合适的链表。large链表用于保存所有超出free和busy页数限制的MSpan。
MHeap示意图:
2.MCache:运行时分配池,不针对全局,而是每个线程都有自己的局部内存缓存MCache,他是实现goroutine高并发的重要因素,因为分配小对象可直接从MCache中分配,不用加锁,提升了并发效率。
type MCache struct
{
tiny byte*; // Allocator cache for tiny objects w/o pointers.
tinysize uintptr;
alloc[NumSizeClasses] MSpan*; // spans to allocate from
};
尽可能将微小对象组合到一个tiny块中,提高性能。
alloc[]用于分配对象,如果没有了,则可以向对应的MCentral获取新的Span进行操作。
线程中分配小对象(16~32K)的过程:
对于
size 介于 16 ~ 32K byte 的内存分配先计算应该分配的 sizeclass,然后去 mcache 里面
alloc[sizeclass] 申请,如果 mcache.alloc[sizeclass] 不足以申请,则 mcache 向 mcentral
申请mcentral 给 mcache 分配完之后会判断自己需不需要扩充,如果需要则想 mheap 申请。
每个线程内申请内存是逐级向上的,首先看MCache是否有足够空间,没有就像MCentral申请,再没有就像MHeap,MHeap向系统申请内存空间。
3.MCentral:作为MHeap和MCache的承上启下的连接。承上,从MHeap申请MSpan;启下,将MSpan划分为各种尺寸的对象提供给MCache使用。
type MCentral struct
{
lock mutex;
sizeClass int32;
noempty mSpanList;
empty mSpanList;
int32 nfree;
……
};
type mSpanList struct {
first *mSpan
last *mSpan
};
sizeclass: 也有成员 sizeclass,用于将MSpan进行切分。
lock: 因为会有多个 P 过来竞争。
nonempty: mspan 的双向链表,当前 mcentral 中可用的 mSpan list。
empty: 已经被使用的,可以认为是一种对所有 mSpan 的 track。MCentral存在于MHeap内。
给对象 object 分配内存的主要流程:
1.object size 32K,则使用 mheap 直接分配。
2.object size 16 byte,使用 mcache 的小对象分配器 tiny 直接分配。 (其实 tiny 就是一个指针,暂且这么说吧。)
3.object size 16 byte size =32K byte 时,先使用 mcache 中对应的 size class 分配。
4.如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。
5.如果 mcentral 也没有可用的块,则向 mheap 申请,并切分。
6.如果 mheap 也没有合适的 span,则想操作系统申请。
tcmalloc内存分配器介绍
tcmalloc(thread-caching mallo)是google推出的一种内存分配器。
具体策略:全局缓存堆和进程的私有缓存。
1.对于一些小容量的内存申请试用进程的私有缓存,私有缓存不足的时候可以再从全局缓存申请一部分作为私有缓存。
2.对于大容量的内存申请则需要从全局缓存中进行申请。而大小容量的边界就是32k。缓存的组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。
golang语言中MHeap就是全局缓存堆,MCache作为线程私有缓存。
在文章开头说过,内存池就是利用MHeap实现,大小切分则是在申请内存的时候就做了,同时MCache分配内存时,可以用MCentral去取对应的sizeClass,多线程管理方面则是通过MCache去实现。
总结:
1.MHeap是一个全局变量,负责向系统申请内存,mallocinit()函数进行初始化。如果分配内存对象大于32K直接向MHeap申请。
2.MCache线程级别管理内存池,关联结构体P,主要是负责线程内部内存申请。
3.MCentral连接MHeap与MCache的,MCache内存不够则向MCentral申请,MCentral不够时向MHeap申请内存。
1.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能
在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因: 数据结构应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(如果是对齐的,那么CPU不需要跨越两个操作字,不是对齐的则需要访问两个操作字才能拼接出需要的内存地址)
指针的大小一般是一个机器字的大小
通过Go语言的structlayout工具,可以得出下图
这些类型在之前的 slice 、 map 、 interface 已经介绍过了,也特意强调过,makehmap函数返回的是一个指针,因此map的对齐为一个机器字.
回头看看 sync.pool的防止copy的空结构体字段,也是放在第一位,破案了。
计算机结构可能会要求内存地址 进行对齐;也就是说,一个变量的地址是一个因子的倍数,也就是该变量的类型是对齐值。
函数Alignof接受一个表示任何类型变量的表达式作为参数,并以字节为单位返回变量(类型)的对齐值。对于变量x:
这是因为int64在bool之后未对齐。
它是32位对齐的,但不是64位对齐的,因为我们使用的是32位系统,因此实际上只是两个32位值并排在一起。
● 内存对齐是为了cpu更高效访问内存中数据
● 结构体对齐依赖类型的大小保证和对齐保证
● 地址对齐保证是:如果类型 t 的对齐保证是 n,那么类型 t 的每个值的地址在运行时必须是 n 的倍数。
● struct内字段如果填充过多,可以尝试重排,使字段排列更紧密,减少内存浪费
● 零大小字段要避免作为struct最后一个字段,会有内存浪费
● 32位系统上对64位字的原子访问要保证其是8bytes对齐的;当然如果不必要的 话,还是用加锁(mutex)的方式更清晰简单
图解go-内存对齐
doc-pdf
在 Golang 游戏leaf系列(一) 概述与示例 (下文简称系列一)中,提到过Go模块用于创建能够被 Leaf 管理的 goroutine。Go模块是对golang中go提供一些额外功能。Go提供回调功能,LinearContext提供顺序调用功能。善用 goroutine 能够充分利用多核资源,Leaf 提供的 Go 机制解决了原生 goroutine 存在的一些问题:
我们来看一个例子(可以在 LeafServer 的模块的 OnInit 方法中测试):
这里的 Go 方法接收 2 个函数作为参数,第一个函数会被放置在一个新创建的 goroutine 中执行,在其执行完成之后,第二个函数会在当前 goroutine 中被执行。由此,我们可以看到变量 res 同一时刻总是只被一个 goroutine 访问,这就避免了同步机制的使用。Go 的设计使得 CPU 得到充分利用,避免操作阻塞当前 goroutine,同时又无需为共享资源同步而忧心。
这里主动调用了 d.Cb(-d.ChanCb) ,把这个回调取出来了。实际上,在skeleton.Run里会自己取这个通道
看一下源码:
New方法,会生成指定缓冲长度的ChanCb。然后调用Go方法就是先执行第一个func,然后把第二个放到Cb里。现在手动造一个例子:
这里解释一下,d.Go根据源码来看,实际也是调用了一个协程。然后上面两次d.Go并不能保证先后顺序。目前的输出结果是1+2那个先执行了,把3写入d.ChanCb,然后把3读出来,继续读时,d.ChanCb里没有东西,阻塞了。然后1+1那个协程启动了,最后又读到了2。
现在把time.Sleep(time.Second)的注释解开,会是啥结果呢
这里执行到time.Sleep睡着了,上面两个d.Go仍然是不确定顺序的,但是会各自的function先执行掉,然后陆续把cb写入d.ChanCb。看这次输出,1+2先写进去的。所以最后执行d.Cb时,就把3先读出来了。然后d.ChanCb的长度为1,说明还有一个,就是输出2了。
另外,就是close时会判断g.pendingGo
这个例子的意思很明显,NewLinearContext这种方式,即使先调用的慢了半秒,它还是会先执行完。
这里先是用了一个list,加入的时候用mutexLinearGo锁了,都加到最后。然后新开协程去处理,读的时候从最前面开始读,也要用mutexLinearGo锁。执行的时候,也要上锁mutexExecution,确保f()执行完并且写入g.ChanCb回调,这个mutexExecution锁才会解除。现在可以改造一个带回调的例子:
结果说明,确实是2先被写入了d.ChanCb。
做了一个参考实例。假设某线程占用时间5秒,超时时间为2秒
func mian() {
lock := sync.Mutex{}
lock.Lock()
defer lock.Unlock()
timer := time.NewTimer(2 * time.Second)
end:=make(chan int)
go func() {
time.Sleep(5*time.Second)
fmt.Println("wait")
end-1
}()
select {
case -end:
case -timer.C:
}
fmt.Println("End")
}
本文是对 Gopher 2017 中一个非常好的 Talk�: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的学习笔记,希望能够通过对 channel 的关键特性的理解,进一步掌握其用法细节以及 Golang 语言设计哲学的管窥蠡测。
channel 是可以让一个 goroutine 发送特定值到另一个 gouroutine 的通信机制。
原生的 channel 是没有缓存的(unbuffered channel),可以用于 goroutine 之间实现同步。
关闭后不能再写入,可以读取直到 channel 中再没有数据,并返回元素类型的零值。
gopl/ch3/netcat3
首先从 channel 是怎么被创建的开始:
在 heap 上分配一个 hchan 类型的对象,并将其初始化,然后返回一个指向这个 hchan 对象的指针。
理解了 channel 的数据结构实现,现在转到 channel 的两个最基本方法: sends 和 receivces ,看一下以上的特性是如何体现在 sends 和 receives 中的:
假设发送方先启动,执行 ch - task0 :
如此为 channel 带来了 goroutine-safe 的特性。
在这样的模型里, sender goroutine - channel - receiver goroutine 之间, hchan 是唯一的共享内存,而这个唯一的共享内存又通过 mutex 来确保 goroutine-safe ,所有在队列中的内容都只是副本。
这便是著名的 golang 并发原则的体现:
发送方 goroutine 会阻塞,暂停,并在收到 receive 后才恢复。
goroutine 是一种 用户态线程 , 由 Go runtime 创建并管理,而不是操作系统,比起操作系统线程来说,goroutine更加轻量。
Go runtime scheduler 负责将 goroutine 调度到操作系统线程上。
runtime scheduler 怎么将 goroutine 调度到操作系统线程上?
当阻塞发生时,一次 goroutine 上下文切换的全过程:
然而,被阻塞的 goroutine 怎么恢复过来?
阻塞发生时,调用 runtime sheduler 执行 gopark 之前,G1 会创建一个 sudog ,并将它存放在 hchan 的 sendq 中。 sudog 中便记录了即将被阻塞的 goroutine G1 ,以及它要发送的数据元素 task4 等等。
接收方 将通过这个 sudog 来恢复 G1
接收方 G2 接收数据, 并发出一个 receivce ,将 G1 置为 runnable :
同样的, 接收方 G2 会被阻塞,G2 会创建 sudoq ,存放在 recvq ,基本过程和发送方阻塞一样。
不同的是,发送方 G1如何恢复接收方 G2,这是一个非常神奇的实现。
理论上可以将 task 入队,然后恢复 G2, 但恢复 G2后,G2会做什么呢?
G2会将队列中的 task 复制出来,放到自己的 memory 中,基于这个思路,G1在这个时候,直接将 task 写到 G2的 stack memory 中!
这是违反常规的操作,理论上 goroutine 之间的 stack 是相互独立的,只有在运行时可以执行这样的操作。
这么做纯粹是出于性能优化的考虑,原来的步骤是:
优化后,相当于减少了 G2 获取锁并且执行 memcopy 的性能消耗。
channel 设计背后的思想可以理解为 simplicity 和 performance 之间权衡抉择,具体如下:
queue with a lock prefered to lock-free implementation:
比起完全 lock-free 的实现,使用锁的队列实现更简单,容易实现
文章名称:go语言mutex的简单介绍
文章链接:https://www.cdcxhl.com/article34/dseiipe.html
成都网站建设公司_创新互联,为您提供面包屑导航、网站排名、建站公司、网站设计、App开发、搜索引擎优化
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联