Skip to content

Goroutines

Every method of Bar is safe to call from many goroutines at once. Workers can share one bar and advance it as each finishes a piece of work, with no lock of your own.

examples/parallel/main.go
// Command parallel checksums files on four goroutines that share one bar.
//
// go run ./examples/parallel <file>...
package main
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"os"
"sync"
"github.com/vardius/progress-go"
)
// checksum returns the SHA-256 of each file, hashed by up to workers goroutines at once.
// Each advances the bar when it finishes a file.
func checksum(out io.Writer, paths []string, workers int) ([]string, error) {
sums := make([]string, len(paths))
errs := make([]error, len(paths))
bar := progress.New(int64(len(paths)), progress.Options{Output: out})
bar.Start()
next := make(chan int)
var wg sync.WaitGroup
for range workers {
wg.Go(func() {
for i := range next {
sums[i], errs[i] = sum(paths[i])
bar.Advance(1)
}
})
}
for i := range paths {
next <- i
}
close(next)
wg.Wait()
return sums, errors.Join(append(errs, bar.Stop())...)
}
func sum(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", fmt.Errorf("hash %s: %w", path, err)
}
return hex.EncodeToString(h.Sum(nil)), nil
}
func main() {
log.SetFlags(0)
sums, err := checksum(os.Stderr, os.Args[1:], 4)
for i, s := range sums {
if s != "" {
fmt.Printf("%s %s\n", s, os.Args[i+1])
}
}
if err != nil {
log.Fatal(err)
}
}

The program draws the bar to standard error, so the checksums on standard output stay clean for a pipe.

Advance is one atomic add on the bar’s counter. On one goroutine it takes about 2 ns and never allocates. When many goroutines advance the same bar at once, they take turns on that one counter, and each call costs more: see Benchmarks. That is still far below the cost of any real piece of work, but if each of your units is tiny, count them locally and advance once per batch:

for i, row := range rows {
insert(row)
if (i+1)%1000 == 0 {
bar.Advance(1000)
}
}
bar.Advance(int64(len(rows) % 1000))

On a terminal, Start draws the bar and sets a timer. Each time it fires, the bar reads the count, draws a frame, and sets the timer again, 100 ms on. Advance takes no part, so however many goroutines call it, and however fast, the terminal gets ten frames a second.

Start, the redraws and Stop share one lock, which also orders the writes. Stop stops the timer and draws the last frame under that lock, so once Stop returns, the bar never writes again, even if a redraw was due.