Problem
Suppose you want to write a function that either handles an event from a channel or logs a warning if no event is received within a specific time frame.
Initial Solution with time.After
We can use time.After
to create a timer inside a for
loop. Because of the surrounding for loop, this timer gets reset on each iteration on receiving successful event or if it fires.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func consumerWithTimeAfter(ch <-chan string) {
for {
select {
case event := <-ch:
handle(event)
case <-time.After(1000 * time.Second):
// Because of the surrounding for loop, this timer gets reset
// on each iteration on receiving successful event
log.Println("warning: no messages received")
}
}
}
|
But this solution has a memory-leak issue. The timer created by time.After
is not garbage collected until it fires. This means that the timer is never garbage collected because it is reset on each iteration. This is a known issue with time.After
and is documented in the time package.
Following snippet helps in understanding the issue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
func getMemory() {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
fmt.Println("Memory Usage:")
fmt.Printf("Alloc = %v MiB", bToMb(memStats.Alloc))
fmt.Printf("\tTotalAlloc = %v MiB", bToMb(memStats.TotalAlloc))
fmt.Printf("\tSys = %v MiB", bToMb(memStats.Sys))
fmt.Printf("\tNumGC = %v\n", memStats.NumGC)
}
func main() {
ch := make(chan string)
go consumerWithTimeAfter(ch)
// Collect memory statistics
ticker := time.NewTicker(5 * time.Second)
go func() {
for {
select {
case <-ticker.C:
getMemory()
}
}
}()
// Simulate events
events := []string{"one", "two", "three"}
ticker2 := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker2.C:
break
default:
ch <- events[0]
ch <- events[1]
ch <- events[2]
time.Sleep(1 * time.Millisecond)
}
}
}
|
Running the above snippet shows that the memory usage keeps increasing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Memory Usage:
Alloc = 2 MiB TotalAlloc = 2 MiB Sys = 16 MiB NumGC = 1
Memory Usage:
Alloc = 5 MiB TotalAlloc = 5 MiB Sys = 25 MiB NumGC = 2
Memory Usage:
Alloc = 8 MiB TotalAlloc = 8 MiB Sys = 25 MiB NumGC = 2
Memory Usage:
Alloc = 10 MiB TotalAlloc = 11 MiB Sys = 30 MiB NumGC = 3
Memory Usage:
Alloc = 13 MiB TotalAlloc = 14 MiB Sys = 34 MiB NumGC = 3
Memory Usage:
Alloc = 15 MiB TotalAlloc = 17 MiB Sys = 34 MiB NumGC = 3
Memory Usage:
Alloc = 17 MiB TotalAlloc = 20 MiB Sys = 38 MiB NumGC = 4
|
Alternative Solution Using time.NewTimer
Instead of using time.After
, we can use time.NewTimer
and reset it on each iteration. This way we do not create a new timer channel at each iteration.
1
2
3
4
5
6
7
8
9
10
11
| func consumerWithResetTimer(ch <-chan string) {
timer := time.NewTimer(1000 * time.Second)
for {
timer.Reset(1000 * time.Second)
select {
case event := <-ch:
handle(event)
case <-timer.C:
log.Println("warning: no messages received")
}
}
|
Replacing consumerAfter
with consumerWithResetTimer
in the above snippet shows that the memory usage is constant.
1
2
3
4
5
6
7
8
9
10
| Memory Usage:
Alloc = 0 MiB TotalAlloc = 0 MiB Sys = 11 MiB NumGC = 0
Memory Usage:
Alloc = 0 MiB TotalAlloc = 0 MiB Sys = 11 MiB NumGC = 0
Memory Usage:
Alloc = 0 MiB TotalAlloc = 0 MiB Sys = 11 MiB NumGC = 0
Memory Usage:
Alloc = 0 MiB TotalAlloc = 0 MiB Sys = 11 MiB NumGC = 0
Memory Usage:
Alloc = 0 MiB TotalAlloc = 0 MiB Sys = 11 MiB NumGC = 0
|
While time.After
is convenient, it’s important to understand its behavior to avoid memory leaks, especially in long-running loops. Using time.NewTimer
and manually resetting the timer is more efficient in such cases.