重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
Go 语言较之 C 语言一个很大的优势就是自带 GC 功能,可 GC 并不是没有代价的。写 C 语言的时候,在一个函数内声明的变量,在函数退出后会自动释放掉,因为这些变量分配在栈上。如果你期望变量的数据可以在函数退出后仍然能被访问,就需要调用 malloc 方法在堆上申请内存,如果程序不再需要这块内存了,再调用 free 方法释放掉。Go 语言不需要你主动调用 malloc 来分配堆空间,编译器会自动分析,找出需要 malloc 的变量,使用堆内存。编译器的这个分析过程就叫做逃逸分析。
创新互联公司从2013年开始,先为民权等服务建站,民权等地企业,进行企业商务咨询服务。为民权企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
所以你在一个函数中通过 dict := make(map[string]int) 创建一个 map 变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的。这要看编译器分析的结果。
可逃逸分析并不是百分百准确的,它有缺陷。有的时候你会发现有些变量其实在栈空间上分配完全没问题的,但编译后程序还是把这些数据放在了堆上。如果你了解 Go 语言编译器逃逸分析的机制,在写代码的时候就可以有意识地绕开这些缺陷,使你的程序更高效。
Go 语言虽然在内存管理方面降低了编程门槛,即使你不了解堆栈也能正常开发,但如果你要在性能上较真的话,还是要掌握这些基础知识。
这里不对堆内存和栈内存的区别做太多阐述。简单来说就是, 栈分配廉价,堆分配昂贵。 栈空间会随着一个函数的结束自动释放,堆空间需要时间 GC 模块不断地跟踪扫描回收。如果对这两个概念有些迷糊,建议阅读下面 2 个文章:
这里举一个小例子,来对比下堆栈的差别:
stack 函数中的变量 i 在函数退出会自动释放;而 heap 函数返回的是对变量 i 的引用,也就是说 heap() 退出后,表示变量 i 还要能被访问,它会自动被分配到堆空间上。
他们编译出来的代码如下:
逻辑的复杂度不言而喻,从上面的汇编中可看到, heap() 函数调用了 runtime.newobject() 方法,它会调用 mallocgc 方法从 mcache 上申请内存,申请的内部逻辑前面文章已经讲述过。堆内存分配不仅分配上逻辑比栈空间分配复杂,它最致命的是会带来很大的管理成本,Go 语言要消耗很多的计算资源对其进行标记回收(也就是 GC 成本)。
Go 编辑器会自动帮我们找出需要进行动态分配的变量,它是在编译时追踪一个变量的生命周期,如果能确认一个数据只在函数空间内访问,不会被外部使用,则使用栈空间,否则就要使用堆空间。
我们在 go build 编译代码时,可使用 -gcflags '-m' 参数来查看逃逸分析日志。
以上面的两个函数为例,编译的日志输出是:
日志中的 i escapes to heap 表示该变量数据逃逸到了堆上。
需要使用堆空间,所以逃逸,这没什么可争议的。但编译器有时会将 不需要 使用堆空间的变量,也逃逸掉。这里是容易出现性能问题的大坑。网上有很多相关文章,列举了一些导致逃逸情况,其实总结起来就一句话:
多级间接赋值容易导致逃逸 。
这里的多级间接指的是,对某个引用类对象中的引用类成员进行赋值。Go 语言中的引用类数据类型有 func , interface , slice , map , chan , *Type(指针) 。
记住公式 Data.Field = Value ,如果 Data , Field 都是引用类的数据类型,则会导致 Value 逃逸。这里的等号 = 不单单只赋值,也表示参数传递。
根据公式,我们假设一个变量 data 是以下几种类型,相应的可以得出结论:
下面给出一些实际的例子:
如果变量值是一个函数,函数的参数又是引用类型,则传递给它的参数都会逃逸。
上例中 te 的类型是 func(*int) ,属于引用类型,参数 *int 也是引用类型,则调用 te(j) 形成了为 te 的参数(成员) *int 赋值的现象,即 te.i = j 会导致逃逸。代码中其他几种调用都没有形成 多级间接赋值 情况。
同理,如果函数的参数类型是 slice , map 或 interface{} 都会导致参数逃逸。
匿名函数的调用也是一样的,它本质上也是一个函数变量。有兴趣的可以自己测试一下。
只要使用了 Interface 类型(不是 interafce{} ),那么赋值给它的变量一定会逃逸。因为 interfaceVariable.Method() 先是间接的定位到它的实际值,再调用实际值的同名方法,执行时实际值作为参数传递给方法。相当于 interfaceVariable.Method.this = realValue
向 channel 中发送数据,本质上就是为 channel 内部的成员赋值,就像给一个 slice 中的某一项赋值一样。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 类型都会导致发送到 channel 中的数据逃逸。
这本来也是情理之中的,发送给 channel 的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配。
可变参数如 func(arg ...string) 实际与 func(arg []string) 是一样的,会增加一层访问路径。这也是 fmt.Sprintf 总是会使参数逃逸的原因。
例子非常多,这里不能一一列举,我们只需要记住分析方法就好,即,2 级或更多级的访问赋值会 容易 导致数据逃逸。这里加上 容易 二字是因为随着语言的发展,相信这些问题会被慢慢解决,但现阶段,这个可以作为我们分析逃逸现象的依据。
下面代码中包含 2 种很常规的写法,但他们却有着很大的性能差距,建议自己想下为什么。
Benchmark 和 pprof 给出的结果:
熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题,并进行优化。
多级间接赋值会导致 Go 编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升。这也是很多人不推荐在 Go 中使用指针的原因,因为它会增加一级访问路径,而 map , slice , interface{} 等类型是不可避免要用到的,为了减少不必要的逃逸,只能拿指针开刀了。
大多数情况下,性能优化都会为程序带来一定的复杂度。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化。
通过var声明或者make函数创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体
源码包中src/runtime/chan.go定义了hchan的数据结构如下:
hchan结构体的主要组成部分有四个:
用来保存goroutine之间传递数据的循环数组:buf
用来记录此循环数组当前发送或接收数据的下标值:sendx和recvx
用于保存向该chan发送和从该chan接收数据被阻塞的goroutine队列: sendq 和 recvq
保证channel写入和读取数据时线程安全的锁:lock
环形数组作为channel 的缓冲区 数组的长度就是定义channnel 时channel 的缓冲大小
在hchan 中包括了读/写 等待队列, waitq是一个双向队列,包括了一个头结点和尾节点。 每个节点是一个sudog结构体变量
channel有2种类型:无缓冲、有缓冲, 在创建时 make(chan type cap) 通过cap 设定缓冲大小
channel有3种模式:写操作模式(单向通道)、读操作模式(单向通道)、读写操作模式(双向通道)
channel有3种状态:未初始化、正常、关闭
如下几种状态会引发panic
channel 是线程安全的,channel的底层实现中,hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据
第一个问题,打开文件应添加"|os.O_WRONLY"
file, err := os.OpenFile("data.dat",os.O_CREATE|os.O_APPEND|os.O_WRONLY,0777)
第二个问题,将Count方法中的 "ch-i"放到方法的最后一行就可以了。
因为一旦“ch-i"执行了,main方法中的 "-ch"就会执行通过当所有的"-ch"执行完后程序就结束了。但这时Count的线程方法还来不及执行完(打开文件的速度相对较慢),所以仅仅执行一次文件操作就结束了
Hello,大家好,又见面了!上一遍我们将 channel 相关基础以及使用场景。这一篇,还需要再次进阶理解channel 阻塞问题。以下创建一个chan类型为int,cap 为3。
channel 内部其实是一个环形buf数据结构 ,是一种滑动窗口机制,当make完后,就分配在 Heap 上。
上面,向 chan 发送一条“hello”数据:
如果 G1 发送数据超过指定cap时,会出现什么情况?
看下面实例:
以上会出现什么,chan 缓冲区允许大小为1,如果再往chan仍数据,满了就会被阻塞,那么是如何实现阻塞的呢?当 chan 满时,会进入 gopark,此时 G1 进入一个 waiting 状态,然后会创建一个 sudog 对象,其实就sendq队列,把 200放进去。等 buf 不满的时候,再唤醒放入buf里面。
通过如下源码,你会更加清晰:
上面,从 chan 获取数据:
Go 语言核心思想:“Do not communicate by sharing memory; instead, share memory by communicating.” 你可以看看这本书名叫:Effective Go
如果接收者,接收一个空对象,也会发生什么情况?
代码示例 :
也会报错如下:
上面,从 chan 取出数据,可是没有数据了。此时,它会把 接收者 G2 阻塞掉,也是和G1发送者一样,也会执行 gopark 将状态改为 waiting,不一样的点就是。
正常情况下,接收者G2作为取出数据是去 buf 读取数据的,但现在,buf 为空了,此时,接收者G2会将sudog导出来,因为现在G2已经被阻塞了嘛,会把G2给G,然后将 t := -ch 中变量 t 是在栈上的地址,放进去 elem ,也就是说,只存它的地址指针在sudog里面。
最后, ch - 200 当G1往 chan 添加200这个数据,正常情况是将数据添加到buf里面,然后唤醒 G2 是吧,而现在是将 G1 的添加200数据直接干到刚才G2阻塞的t这里变量里面。
你会认为,这样真的可以吗?想一想,G2 本来就是已经阻塞了,然后我们直接这么干肯定没有什么毛病,而且效率提高了,不需要再次放入buf再取出,这个过程也是需要时间。不然,不得往chan添加数据需要加锁、拷贝、解锁一序列操作,那肯定就慢了,我想Go语言是为了高效及内存使用率的考虑这样设计的。(注意,一般都是在runtime里面完成,不然会出现象安全问题。)
总结 :
chan 类型的特点:chan 如果为空,receiver 接收数据的时候就会阻塞等待,直到 chan 被关闭或者有新的数据到来。有这种个机制,就可以实现 wait/notify 的设计模式。
相关面试题:
Go提供了一种称为通道的机制,用于在goroutine之间共享数据。当您作为goroutine执行并发活动时,需要在goroutine之间共享资源或数据,通道充当goroutine之间的管道(管道)并提供一种机制来保证同步交换。
根据数据交换的行为,有两种类型的通道:无缓冲通道和缓冲通道。无缓冲通道用于执行goroutine之间的同步通信,而缓冲通道用于执行异步通信。无缓冲通道保证在发送和接收发生的瞬间两个goroutine之间的交换。缓冲通道没有这样的保证。
通道由make函数创建,该函数指定chan关键字和通道的元素类型。
这是创建无缓冲和缓冲通道的代码块:
语法
使用内置函数make创建无缓冲和缓冲通道。make的第一个参数需要关键字chan,然后是通道允许交换的数据类型。
这是将值发送到通道的代码块需要使用-运算符:
语法
一个包含5个值的缓冲区的字符串类型的goroutine1通道。然后我们通过通道发送字符串“Australia”。
这是从通道接收值的代码块:
语法
- 运算符附加到通道变量(goroutine1)的左侧,以接收来自通道的值。
在无缓冲通道中,在接收到任何值之前没有能力保存它。在这种类型的通道中,发送和接收goroutine在任何发送或接收操作完成之前的同一时刻都准备就绪。如果两个goroutine没有在同一时刻准备好,则通道会让执行其各自发送或接收操作的goroutine首先等待。同步是通道上发送和接收之间交互的基础。没有另一个就不可能发生。
在缓冲通道中,有能力在接收到一个或多个值之前保存它们。在这种类型的通道中,不要强制goroutine在同一时刻准备好执行发送和接收。当发送和接收阻塞时也有不同的条件。只有当通道中没有要接收的值时,接收才会阻塞。仅当没有可用缓冲区来放置正在发送的值时,发送才会阻塞。
实例
运行结果
有缓冲chan不容易阻塞
无缓冲chan是同步的,就是在一个协程里面塞(取),另外一个操作必须即刻执行否则就会阻塞
程序的执行是以栈的方式逐步执行的,如此就能知道什么时候会阻塞了!