Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

Follow publication

Demystifying Golang Channels, Goroutines, and Optimal Concurrency

Matt Wiater
Level Up Coding
Published in
12 min readApr 17, 2023

--

The Framework

The Question

An Idealized Scenario

A Rough Estimate

func (job Job) EmptySleepJob() (string, float64) {
jobStartTime := time.Now()

time.Sleep(time.Duration(config.EmptySleepJobSleepTimeMs) * time.Millisecond)

jobEndTime := time.Now()
jobElapsed := jobEndTime.Sub(jobStartTime)

jobResult := structs.SleepJobResult{}
jobResult.SleepTime = time.Duration(config.EmptySleepJobSleepTimeMs).String()
jobResult.Elapsed = jobElapsed.String()
jobResult.Status = strconv.FormatBool(true)

jobResultString, err := json.Marshal(jobResult)
if err != nil {
fmt.Println(err)
}

return string(jobResultString), jobElapsed.Seconds()
}
DEBUG=                // Verbose console output, default: false
JOBNAME=EmptySleepJob // Job: EmptySleepJob (default), PiJob, or IoJob
STARTINGWORKERCOUNT=1 // Workers to start the test with, default: 1
MAXWORKERCOUNT=8 // Workers to ramp up to, default: runtime.NumCPU()
TOTALJOBCOUNT=64 // Jobs to run, default: runtime.NumCPU() * 2
Summary Results: EmptySleepJob
+---------+------+--------------+-------------------+-------------+--------+
| WORKERS | JOBS | AVG JOB TIME | TOTAL WORKER TIME | AVG MEM USE | +/- |
+---------+------+--------------+-------------------+-------------+--------+
| 1 | 64 | 1.00s | 64.35s | 0.004Mb | (1x)* |
| 2 | 64 | 1.01s | 32.23s | 0.005Mb | +2x |
| 3 | 64 | 1.00s | 22.12s | 0.006Mb | +2.91x |
| 4 | 64 | 1.00s | 16.03s | 0.007Mb | +4.01x |
| 5 | 64 | 1.00s | 13.04s | 0.008Mb | +4.93x |
| 6 | 64 | 1.00s | 11.05s | 0.008Mb | +5.82x |
| 7 | 64 | 1.00s | 10.03s | 0.009Mb | +6.42x |
| 8 | 64 | 1.00s | 8.04s | 0.008Mb | +8x |
+---------+------+--------------+-------------------+-------------+--------+

* Baseline: All subsequent +/- tests are compared to this.

A (More) Real-World Scenario

JOBNAME=PiJob
STARTINGWORKERCOUNT=1
MAXWORKERCOUNT=8
TOTALJOBCOUNT=64
Summary Results: PiJob
+---------+------+--------------+-------------------+-------------+--------+
| WORKERS | JOBS | AVG JOB TIME | TOTAL WORKER TIME | AVG MEM USE | +/- |
+---------+------+--------------+-------------------+-------------+--------+
| 1 | 64 | 0.96s | 61.34s | 0.010Mb | (1x)* |
| 2 | 64 | 1.10s | 35.37s | 0.011Mb | +1.73x |
| 3 | 64 | 1.29s | 28.10s | 0.012Mb | +2.18x |
| 4 | 64 | 1.47s | 23.72s | 0.014Mb | +2.59x |
| 5 | 64 | 1.66s | 21.72s | 0.016Mb | +2.82x |
| 6 | 64 | 1.93s | 21.12s | 0.018Mb | +2.9x |
| 7 | 64 | 2.20s | 20.89s | 0.019Mb | +2.94x |
| 8 | 64 | 2.53s | 20.48s | 0.018Mb | +3x |
+---------+------+--------------+-------------------+-------------+--------+

* Baseline: All subsequent +/- tests are compared to this.

Point of Diminishing Returns

JOBNAME=PiJob
STARTINGWORKERCOUNT=1
MAXWORKERCOUNT=32
TOTALJOBCOUNT=64
Summary Results: PiJob
+---------+------+--------------+-------------------+-------------+--------+
| WORKERS | JOBS | AVG JOB TIME | TOTAL WORKER TIME | AVG MEM USE | +/- |
+---------+------+--------------+-------------------+-------------+--------+
| 1 | 64 | 0.97s | 62.14s | 0.009Mb | (1x)* |
| 2 | 64 | 1.10s | 35.34s | 0.010Mb | +1.76x |
| 3 | 64 | 1.27s | 27.86s | 0.011Mb | +2.23x |
| 4 | 64 | 1.51s | 24.33s | 0.013Mb | +2.55x |
| 5 | 64 | 1.63s | 21.29s | 0.019Mb | +2.92x |
| 6 | 64 | 1.90s | 20.87s | 0.019Mb | +2.98x |
| 7 | 64 | 2.27s | 21.46s | 0.024Mb | +2.9x |
| 8 | 64 | 2.51s | 20.41s | 0.026Mb | +3.04x |
| 9 | 64 | 3.01s | 22.32s | 0.021Mb | +2.78x |
| 10 | 64 | 3.01s | 20.14s | 0.023Mb | +3.09x |
| 11 | 64 | 3.38s | 20.28s | 0.021Mb | +3.07x |
| 12 | 64 | 3.92s | 21.96s | 0.020Mb | +2.83x |
| 13 | 64 | 3.96s | 19.95s | 0.025Mb | +3.12x |
| 14 | 64 | 4.15s | 20.04s | 0.023Mb | +3.1x |
| 15 | 64 | 4.63s | 20.94s | 0.026Mb | +2.97x |
| 16 | 64 | 5.06s | 20.51s | 0.025Mb | +3.03x |
| 17 | 64 | 5.18s | 20.59s | 0.024Mb | +3.02x |
| 18 | 64 | 5.36s | 20.51s | 0.023Mb | +3.03x |
| 19 | 64 | 5.56s | 20.18s | 0.025Mb | +3.08x |
| 20 | 64 | 6.01s | 20.60s | 0.023Mb | +3.02x |
| 21 | 64 | 6.48s | 20.66s | 0.028Mb | +3.01x |
| 22 | 64 | 6.45s | 19.68s | 0.036Mb | +3.16x |
| 23 | 64 | 6.57s | 19.63s | 0.034Mb | +3.17x |
| 24 | 64 | 6.64s | 19.65s | 0.028Mb | +3.16x |
| 25 | 64 | 6.72s | 19.17s | 0.031Mb | +3.24x |
| 26 | 64 | 7.01s | 19.37s | 0.033Mb | +3.21x |
| 27 | 64 | 7.02s | 18.66s | 0.038Mb | +3.33x |
| 28 | 64 | 7.43s | 18.98s | 0.033Mb | +3.27x |
| 29 | 64 | 7.47s | 18.19s | 0.042Mb | +3.42x |
| 30 | 64 | 7.90s | 18.42s | 0.037Mb | +3.37x |
| 31 | 64 | 7.99s | 17.86s | 0.039Mb | +3.48x |
| 32 | 64 | 8.37s | 17.31s | 0.039Mb | +3.59x |
+---------+------+--------------+-------------------+-------------+--------+

* Baseline: All subsequent +/- tests are compared to this.

Conclusion

...
iterations := 11
for n := 0; n < iterations; n++ {
f, err := os.Create("/tmp/test.txt")
if err != nil {
panic(err)
}
for i := 0; i < 100000; i++ {
f.WriteString("some text!\n")
}
f.Close()
}
...
JOBNAME=IoJob
STARTINGWORKERCOUNT=1
MAXWORKERCOUNT=8
TOTALJOBCOUNT=16
Summary Results: IoJob
+---------+------+--------------+-------------------+-------------+--------+
| WORKERS | JOBS | AVG JOB TIME | TOTAL WORKER TIME | AVG MEM USE | +/- |
+---------+------+--------------+-------------------+-------------+--------+
| 1 | 16 | 1.03s | 16.50s | 0.004Mb | (1x)* |
| 2 | 16 | 1.53s | 12.26s | 0.004Mb | +1.35x |
| 3 | 16 | 2.27s | 12.74s | 0.004Mb | +1.29x |
| 4 | 16 | 2.94s | 11.80s | 0.004Mb | +1.4x |
| 5 | 16 | 3.71s | 12.63s | 0.005Mb | +1.31x |
| 6 | 16 | 3.93s | 11.44s | 0.005Mb | +1.44x |
| 7 | 16 | 5.53s | 13.33s | 0.005Mb | +1.24x |
| 8 | 16 | 6.68s | 13.59s | 0.006Mb | +1.21x |
+---------+------+--------------+-------------------+-------------+--------+

* Baseline: All subsequent +/- tests are compared to this.

--

--

Written by Matt Wiater

Software Engineer, currently exploring Golang and posting articles on my findings and discoveries as I dive deeper into the language.

Responses (1)