Ontem à noite o vento oeste murchou as árvores verdes
Nos quatro principais usos do controle select/#timeout em Golang , é mencionado que select é combinado time.After
para realizar o controle de tempo limite. Na verdade, isso é um problema.
Uma vez que este método de escrita irá inicializar um novo tempo. Após cada tempo, quando o tempo de espera for longo, como 1 minuto, ocorrerá um vazamento de memória (claro, o problema não se limita a isso, continue lendo)
Não sei quem o trouxe. O uso de select e time.After para controle de tempo limite em Go quase se tornou um padrão de fato. Exemplos como
Golang time.After() e exemplos de código abundam na Internet
Em muitos grandes repositórios de código de empresa, <- time.After
há muitas palavras-chave para uma pesquisa e muitas das seguintes duram alguns minutos.
Não é difícil descobrir com pprof, este é um erro em nível de livro didático... Ele é inicializado todas as vezes, mas não será reciclado antes da execução, fazendo com que a memória dispare.
Recomenda-se pesquisar quando tiver tempo para ver se há muito uso desse tipo no código...
pode referir-se
Fengyun-Analisar o problema OOM da explosão de memória causada pelo golang time.After
Usar time.After com cuidado causará vazamentos de memória (golang)
Essas análises, a verificação real
after.go:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
/**
time.After oom 验证demo
*/
func main() {
go func() {
// 开启pprof,监听请求
if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
}
}()
ch := make(chan string, 100)
go func() {
for {
ch <- "向管道塞入数据"
}
}()
for {
select {
case <-ch:
case <-time.After(time.Minute * 3):
}
}
}
execute este programa e, em seguida, execute
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
Esta linha de comando pode ser dividida em três partes:
-
-http=:8081
Ele é especificado na forma de web e inicia na porta local 8081
(se nenhum-http=:8081
parâmetro for adicionado, ele entrará na interação da linha de comando, e inseri-lo na linha de comando equivaleweb
a usar os-http=:8081
parâmetros diretamente) -
http://localhost:6060/debug/pprof/heap
É o endereço a ser especificado para obter o arquivo de perfil. Programas locais rodando em tempo real podem usar este método. Em mais casos (como no servidor, não há porta para pprof aberta para o mundo externo), você pode ir para a máquina primeiro, usácurl http://127.0.0.1:6060/debug/pprof/heap -o heap_cui.out
-la para obter o arquivo de perfil, e, em seguida, encontre uma maneira de obtê-lo localmente. , usandogo tool pprof --http :9091 heap_cui.out
a análise
E com o tempo, a memória ocupada pelo programa continuará aumentando
从调用图可发现, 程序不断调用time.After,进而导致计时器 time.NerTimer
不断创建和内存申请
// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
//After 等待持续时间过去,然后在返回的通道上发送当前时间。
//它相当于 NewTimer(d).C。
//在定时器触发之前,垃圾收集器不会恢复底层定时器。 如果效率是一个问题,请改用 NewTimer 并在不再需要计时器时调用 Timer.Stop。
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
在select里面虽然没有执行到time.After,但每次都会初始化,会在时间堆里面,定时任务未到期之前,是不会被gc清理的
-
在计时器触发之前,垃圾收集器不会回收Timer
-
如果考虑效率,需要使用NewTimer替代
衣带渐宽终不悔
使用NewTimer 或NewTicker替代:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
/**
time.After oom 验证demo
*/
func main() {
go func() {
// 开启pprof,监听请求
if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
}
}()
ticker := time.NewTicker(time.Minute * 3)
// 或
//timer := time.NewTimer(3 * time.Minute)
//defer timer.Stop()
// 下方的case <-ticker.C:相应改为case <-timer.C:
ch := make(chan string, 100)
go func() {
for {
ch <- "向管道塞入数据"
}
}()
for {
select {
case <-ch:
case <-ticker.C:
print("结束执行")
}
}
}
这篇Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了!应该有点问题,不是内存孤儿,gc还是会去回收的,只是要在time.After到期之后
众里寻他千百度
如上是网上大多数技术文章的情况:
-
昨夜西风凋碧树,独上高楼,望断天涯路: select + time.After实现超时控制
-
衣带渐宽终不悔,为伊消得人憔悴: 这样写有问题,会内存泄露,要用NewTimer 或NewTicker替代time.After
其实针对本例,这些说法都没有切中肯綮
最初的代码仅仅是有内存泄露的问题吗?
实际上,即便3分钟后,第2个case也得不到执行 (可以把3min改成2s验证下)
只要第一个case能不断从channel中取出数据(在此显然可以),那第二个case就永远得不到执行。这是因为每次time.After都被重新初始化了,而上面那个case一直满足条件,当然就是第二个case一直得不到执行, 除非第一个case超过3min没有从channel中拿到数据
所以其实在此例中NewTimer还是NewTicker,都不是问题本质,这个问题本质,就是个变量作用域的问题
在for循环外定义time.After(time.Minute * 3),如下:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
go func() {
// 开启pprof,监听请求
if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
}
}()
ch := make(chan string, 100)
go func() {
for {
ch <- "向管道塞入数据"
}
}()
timeout := time.After(time.Minute * 3)
for {
select {
case <-ch:
case <-timeout:
fmt.Println("到了这里")
}
}
}
Coloque time.Após fora do loop, você pode ver que não há vazamento de memória.Após 3 minutos (talvez um pouco mais), o segundo caso é executado conforme programado.
Portanto, neste cenário, não é hora.
O coletor de lixo não reciclará o Timer até que o timer seja acionado
O problema, mas pelo menos o problema de escopo de variável mais ignorado.
(O pote do programador não é o problema do tempo. Depois... A razão pela qual NewTimer ou NewTicker não vazará memória é apenas porque é inicializado fora do loop for...)
case <-time.After(time.Minute * 3)
A escrita anterior no loop for, o efeito é semelhante ao seguinte:
package main
import "time"
func main() {
for {
time.After(2 * time.Second)
}
}
Verifique se ele se tornará um chamado "órfão de memória"
Modifique o programa para verificar:
O coletor de lixo não reciclará o Timer até que o timer seja acionado
;
mas em
Após o disparo do temporizador, o coletor de lixo reciclará esses temporizadores
, não causa "órfãos de memória"
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"sync/atomic"
"time"
)
func main() {
go func() {
// 开启pprof,监听请求
if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
}
}()
after()
fmt.Println("程序结束")
}
func after() {
var i int32
ch := make(chan string, 0)
done := make(chan string) // 设定的时间已到,通知结束循环,不要再往channel里面写数据
go func() {
for {
select {
default:
atomic.AddInt32(&i, 1)
ch <- fmt.Sprintf("%s%d%s", "向管道第", i, "次塞入数据")
case exit := <-done:
fmt.Println("关闭通道", exit)
return
}
}
}()
go func() {
time.Sleep(time.Second)
done <- "去给我通知不要再往ch这个channel里写数据了!"
}()
for {
select {
case res := <-ch:
fmt.Println("res:", res)
case <-time.After(2 * time.Second):
fmt.Println("结束接收通道的数据")
return
}
}
}
Remova as informações impressas e substitua-as pelas informações atuais da memória em tempo real:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"runtime"
"sync/atomic"
"time"
)
func main() {
go func() {
// 开启pprof,监听请求
if err := http.ListenAndServe(":6060", nil); err != nil { // 也可以写成 127.0.0.1:6060
fmt.Printf("start pprof failed on %s,err%v \n", "6060", err)
}
}()
after()
fmt.Println("程序结束")
}
func after() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Println("before, have", runtime.NumGoroutine(), "goroutines,", ms.Alloc, "bytes allocated", ms.HeapObjects, "heap object")
var i int32
ch := make(chan string, 0)
done := make(chan string) // 设定的时间已到,通知结束循环,不要再往channel里面写数据
go func() {
for {
select {
default:
atomic.AddInt32(&i, 1)
ch <- fmt.Sprintf("%s%d%s", "向管道第", i, "次塞入数据")
case exit := <-done:
fmt.Println("关闭通道", exit)
return
}
}
}()
go func() {
time.Sleep(time.Second)
done <- "去给我通知不要再往ch这个channel里写数据了!"
}()
for {
select {
case res := <-ch:
runtime.GC()
runtime.ReadMemStats(&ms)
fmt.Printf("%s,now have %d goroutines,%d bytes allocated, %d heap object \n", res, runtime.NumGoroutine(), ms.Alloc, ms.HeapObjects)
case <-time.After(2 * time.Second):
runtime.GC()
fmt.Println("当前结束接收通道的数据,准备返程")
runtime.ReadMemStats(&ms)
fmt.Printf("now have %d goroutines,%d bytes allocated, %d heap object \n", runtime.NumGoroutine(), ms.Alloc, ms.HeapObjects)
return
}
}
}
Mais referências: