GolangNote

Golang笔记

Golang 减小gc 压力、避免内存泄漏小tips

Permalink

一些减小 gc 压力、避免内存泄漏的小技巧分享。

减少对象分配

所谓减少对象的分配,实际上是尽量做到,对象的重用。 比如像如下的两个函数定义:

Go: 减少对象分配
1
2
3
func(r*Reader)Read()([]byte, error)
func(r*Reader)Read(buf []byte)(int, error) // ok 有形参

第一个函数没有形参,每次调用的时候返回一个 []byte ,第二个函数在每次调用的时候,形参是一个 buf []byte 类型的对象,之后返回读入的byte的数目。

第一个函数在每次调用的时候都会分配一段空间,这会给 gc 造成额外的压力。第二个函数在每次调用的时候,会重用形参声明。

避免 string[]byte 转化

老生常谈 string[]byte 转化,在 stirng[]byte 之间进行转换,会给 gc 造成压力 通过 gdb ,可以先对比下两者的数据结构:

Go: string 和 byte
1
2
type = struct []uint8 {    uint8 *array;    int len;    int cap;}
type = struct string {    uint8 *str;    int len;}

两者发生转换的时候,底层数据结结构会进行复制,因此导致 gc 效率会变低。解决策略上,一种方式是一直使用 []byte ,特别是在数据传输方面,[]byte 中也包含着许多 string 会常用到的有效的操作。另一种是使用更为底层的操作直接进行转化,避免复制行为的发生,使用 unsafe.Pointer 直接进行转化。

字符串连接

尽少使用 + 连接 string 由于采用 + 来进行 string 的连接会生成新的对象,降低 gc 的效率,好的方式是通过 append 函数来进行。

但是还有一个弊端,比如参考如下代码:

Go: append
1
2
3
b := make([]int, 1024)
b = append(b, 99)
fmt.Println("len:", len(b), "cap:", cap(b))

指定 slice 长度

在使用了 append 操作之后,数组的空间由 1024 增长到了 1312 ,所以如果能提前知道数组的长度的话,最好在最初分配空间的时候就做好空间规划操作,会增加一些代码管理的成本,同时也会降低 gc 的压力,提升代码的效率。

对上面的代码可以这样改进:

Go: 指定长度
1
b := make([]int, 0, 1024)

内存泄漏大部分原因是代码里新开的协程有引用地方,导致GC无法释放。

避免内存泄漏的两个原则

1、 绝对不能由消费者关 channel,因为向关闭的 channel 写数据会 panic。正确的姿势是生产者写完所有数据后,关闭 channel,消费者负责消费完 channel 里面的全部数据:

Go: channel关闭
1
2
3
4
5
6
7
8
9
10
11
func produce(ch chan<- T) {
    defer close(ch) // 生产者写完数据关闭 channel
    ch <- T{}
}
func consume(ch <-chan T) {
    for _ = range ch { // 消费者用for-range读完里面所有数据
    }
}
ch := make(chan T)
go produce(ch)
consume(ch)

为什么 consume 要读完 channel 里面所有数据?因为 go produce()可能有多个,这样写的代码,在读完ch可以确定所有produce的goroutine都退出了,不会泄漏。

2、 利用关闭 channel 来广播取消动作。向关闭的 channel 读数据永远不会阻塞,这是进阶的技巧。假设消费者拿到数据处理后有 error 发生,整个动作失败,那么需要有某种机制通知生产者停止并退出。

Go: 改进
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func produce(ch chan<- T, cancel chan struct{}) {
    select {
      case ch <- T{}:
      case <- cancel: // 用 select 同时监听 cancel 动作
    }
}
func consume(ch <-chan T, cancel chan struct{}) {
    v := <-ch
    err := doSomeThing(v)
    if err != nil {
        close(cancel) // 能够通知所有produce退出
        return
    }
}
for i:=0; i<10; i++ {
    go produce()
}
consume()

WaitGroup 之类的可以配合着用,看自己喜欢的风格。基本上能处理好 error 场景下的资源释放,问题就不大。 对于并发的代码心存敬畏之心,哪怕用 Go,哪怕有 channel 这么好用的东西!

相关参考:

本文网址: https://golangnote.com/topic/222.html 转摘请注明来源

Related articles

Golang 单实例实现网站多域名请求

有时候写网站,为了统一的后端,把不同业务都集中到一个后端,这时就需要处理多域名的请求,在 Go http server 里实现很简单,只需把不同域名映射到不同的 `http.Handler`。...

Write a Comment to "Golang 减小gc 压力、避免内存泄漏小tips"

Submit Comment Login
Based on Golang + fastHTTP + sdb | go1.22.3 Processed in 1ms