Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is golang channel memory usage dynamic?

Tags:

go

I have test go channel memory usage and found that it differs from channel input frequency while the amount of goroutines are the same.

As the code below, I create thousands of goroutines, which produce data to its own channel and consume data from same channel.

By changing only the variable "interval" of producer, I can see the virtual memory and resident memory varies too by monitoring with command "top".

And the shorter is the interval, the usage of memory is larger.

Does anyone know what happen?

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

type Session struct {
    KeepAlive chan bool
}

var count = 1024 * 8 * 4

var interval = 250 * time.Millisecond //3718.0m 3.587g   1.2m S 224.0 23.1

// var interval = 500 * time.Millisecond //2011.2m 1.923g   1.2m S 118.8 12.4

// var interval = 1 * time.Second   //1124.0m 1.059g   1.1m S  73.0  6.8

func main() {

    var gracefulStop = make(chan os.Signal, 1)
    signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL)

    for i := 0; i < count; i++ {
        go Loop()
    }

    <-gracefulStop
    fmt.Println("gracefulStop")
}

func Loop() (err error) {

    var se *Session
    se = NewSession()

    se.Serve()

    return
}

func NewSession() (s *Session) {

    fmt.Println("NewSession")

    s = &Session{

        KeepAlive: make(chan bool, 1),
    }

    return
}

func (s *Session) Serve() {

    fmt.Println("Serve")

    go s.sendLoop()

    s.readLoop()

    s.Close()

    return
}

func (s *Session) Close() {

    close(s.KeepAlive)
    fmt.Println("Close")
}

// local-------------------------------------------------------

func (s *Session) readLoop() {
    fmt.Println("readLoop")

    sec := time.Duration(1 * time.Minute)

ServerHandlerLoop:
    for {
        select {

        case alive := <-s.KeepAlive:
            if alive == false {
                break ServerHandlerLoop
            }

        case <-time.After(sec):
            fmt.Println("Timeout")
            break ServerHandlerLoop

        }
    }

    fmt.Println("readLoop EXIT")
}

func (s *Session) sendLoop() {

    for {

        s.KeepAlive <- true

        time.Sleep(interval)

    }

    s.KeepAlive <- false

    fmt.Println("ReadMessage EXIT")
}
like image 346
Wolfer Avatar asked Mar 05 '23 20:03

Wolfer


1 Answers

pprof can tell you where you spend memory. Simply add an import statement for the net/http/pprof package and start an HTTP server with the http.DefaultServeMux:

import _ "net/http/pprof"

func main() {
    go func() { log.Fatal(http.ListenAndServe(":4000", nil)) }()

    //...
}

While the program is running, run the pprof tool to look at various statistics about your program. Since you are concerned about memory usage, the heap profile (memory-in-use) is probably most relevant.

$ go tool pprof -top 10 http://localhost:4000/debug/pprof/heap
Fetching profile over HTTP from http://localhost:4000/debug/pprof/heap
File: foo
Build ID: 10
Type: inuse_space
Time: Dec 21, 2018 at 12:52pm (CET)
Showing nodes accounting for 827.57MB, 99.62% of 830.73MB total
Dropped 9 nodes (cum <= 4.15MB)
      flat  flat%   sum%        cum   cum%
  778.56MB 93.72% 93.72%   796.31MB 95.86%  time.NewTimer
   18.25MB  2.20% 95.92%    18.25MB  2.20%  time.Sleep
   17.75MB  2.14% 98.05%    17.75MB  2.14%  time.startTimer
      11MB  1.32% 99.38%       11MB  1.32%  runtime.malg
       2MB  0.24% 99.62%   798.31MB 96.10%  main.(*Session).readLoop
         0     0% 99.62%   798.31MB 96.10%  main.(*Session).Serve
         0     0% 99.62%    18.25MB  2.20%  main.(*Session).sendLoop
         0     0% 99.62%   800.81MB 96.40%  main.Loop
         0     0% 99.62%    11.67MB  1.40%  runtime.mstart
         0     0% 99.62%    11.67MB  1.40%  runtime.newproc.func1
         0     0% 99.62%    11.67MB  1.40%  runtime.newproc1
         0     0% 99.62%    11.67MB  1.40%  runtime.systemstack
         0     0% 99.62%   796.31MB 95.86%  time.After

Unsurprisingly, the huge amount of time.Timers you are creating with time.After accounts for pretty much all of the memory in use.

Think about it: With an interval of 250ms you are creating timers 4 times faster than with an interval of 1s. However, the lifetime of the timers is not proportional to the interval -- it is constant at 60 seconds. So at any given point you have 4*60=240 times more timers alive.

From the docs of time.After:

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.

So create a single timer per readLoop and re-use it. You can further reduce memory usage by using a channel of empty struct values instead of a channel of booleans:

type Session struct {
    KeepAlive chan struct{}
}

func (s *Session) readLoop() {
    fmt.Println("readLoop")

    d := 1 * time.Minute
    t := time.NewTimer(d)

loop:
    for {
        select {
        case _, ok := <-s.KeepAlive:
            if !ok {
                break loop
            }

            if !t.Stop() {
                <-t.C
            }
            t.Reset(d)

        case <-t.C:
            fmt.Println("Timeout")
            break loop
        }
    }

    fmt.Println("readLoop EXIT")
}

func (s *Session) sendLoop() {
    defer close(s.KeepAlive)

    for {
        s.KeepAlive <- struct{}{}
        time.Sleep(interval)
    }
}
like image 123
Peter Avatar answered Mar 09 '23 07:03

Peter