Documentation
¶
Overview ¶
Package taskgroup manages collections of cooperating goroutines. It defines a Group that handles waiting for goroutine termination and the propagation of error values. The caller may provide a callback to filter and respond to task errors.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Collector ¶ added in v0.7.0
type Collector[T any] struct { // contains filtered or unexported fields }
A Collector collects values reported by task functions and delivers them to an accumulator function.
Example ¶
package main
import (
"errors"
"fmt"
"math/rand/v2"
"github.com/creachadair/taskgroup"
)
func main() {
var total int
c := taskgroup.NewCollector(func(v int) {
total += v
})
const numTasks = 25
input := rand.Perm(500)
// Start a bunch of tasks to find elements in the input...
g := taskgroup.New(nil)
for i := 0; i < numTasks; i++ {
target := i + 1
g.Go(c.Task(func() (int, error) {
for _, v := range input {
if v == target {
return v, nil
}
}
return 0, errors.New("not found")
}))
}
// Wait for the searchers to finish, then signal the collector to stop.
g.Wait()
// Now get the final result.
fmt.Println(total)
}
Output: 325
func NewCollector ¶ added in v0.7.0
NewCollector creates a new collector that delivers task values to the specified accumulator function. The collector serializes calls to value, so that it is safe for the function to access shared state without a lock.
The tasks created from a collector do not return until all the values reported by the underlying function have been processed by the accumulator.
func (*Collector[T]) NoError ¶ added in v0.7.0
NoError returns a Task wrapping a call to f. The resulting task reports a nil error for all calls.
func (*Collector[T]) Report ¶ added in v0.8.0
Report returns a task wrapping a call to f, which is passed a function that sends results to the accumulator. The report function does not return until the accumulator has finished processing the value.
Example ¶
package main
import (
"errors"
"fmt"
"log"
"github.com/creachadair/taskgroup"
)
func main() {
type val struct {
who string
v int
}
c := taskgroup.NewCollector(func(z val) { fmt.Println(z.who, z.v) })
err := taskgroup.New(nil).
// The Report method passes its argument a function to report multiple
// values to the collector.
Go(c.Report(func(report func(v val)) error {
for i := 0; i < 3; i++ {
report(val{"even", 2 * i})
}
return nil
})).
// Multiple reporters are fine.
Go(c.Report(func(report func(v val)) error {
for i := 0; i < 3; i++ {
report(val{"odd", 2*i + 1})
}
// An error from a reporter is propagated like any other task error.
return errors.New("no bueno")
})).
Wait()
if err == nil || err.Error() != "no bueno" {
log.Fatalf("Unexpected error: %v", err)
}
}
Output: even 0 odd 1 even 2 odd 3 even 4 odd 5
type ErrorFunc ¶
An ErrorFunc is called by a group each time a task reports an error. Its return value replaces the reported error, so the ErrorFunc can filter or suppress errors by modifying or discarding the input error.
func Listen ¶
Listen creates an ErrorFunc that reports each non-nil task error to f. The resulting ErrorFunc returns task errors unmodified.
Example ¶
package main
import (
"errors"
"fmt"
"log"
"strings"
"github.com/creachadair/taskgroup"
)
func main() {
// The taskgroup itself will only report the first non-nil task error, but
// you can use an error listener used to accumulate all of them.
var all []error
g := taskgroup.New(taskgroup.Listen(func(e error) {
all = append(all, e)
}))
g.Go(func() error { return errors.New("badness 1") })
g.Go(func() error { return errors.New("badness 2") })
g.Go(func() error { return errors.New("badness 3") })
if err := g.Wait(); err == nil || !strings.Contains(err.Error(), "badness") {
log.Fatalf("Unexpected error: %v", err)
}
fmt.Println(errors.Join(all...))
}
Output: badness 1 badness 2 badness 3
func Trigger ¶
func Trigger(f func()) ErrorFunc
Trigger creates an ErrorFunc that calls f each time a task reports an error. The resulting ErrorFunc returns task errors unmodified.
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/creachadair/taskgroup"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
const badTask = 5
// Construct a group in which any task error cancels the context.
g := taskgroup.New(taskgroup.Trigger(cancel))
for i := 0; i < 10; i++ {
i := i
g.Go(func() error {
if i == badTask {
return fmt.Errorf("task %d failed", i)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(10 * time.Second):
return nil
}
})
}
if err := g.Wait(); err == nil {
log.Fatal("I expected an error here")
} else {
fmt.Println(err.Error())
}
}
Output: task 5 failed
type Group ¶
type Group struct {
// contains filtered or unexported fields
}
A Group manages a collection of cooperating goroutines. Add new tasks to the group with the Go method. Call the Wait method to wait for the tasks to complete. A zero value is ready for use, but must not be copied after its first use.
The group collects any errors returned by the tasks in the group. The first non-nil error reported by any task (and not otherwise filtered) is returned from the Wait method.
Example ¶
package main
import (
"fmt"
"github.com/creachadair/taskgroup"
)
func main() {
msg := make(chan string)
g := taskgroup.New(nil)
g.Go(func() error {
msg <- "ping"
fmt.Println(<-msg)
return nil
})
g.Go(func() error {
fmt.Println(<-msg)
msg <- "pong"
return nil
})
g.Wait()
fmt.Println("<done>")
}
Output: ping pong <done>
func New ¶
New constructs a new empty group. If ef != nil, it is called for each error reported by a task running in the group. The value returned by ef replaces the task's error. If ef == nil, errors are not filtered.
Calls to ef are issued by a single goroutine, so it is safe for ef to manipulate local data structures without additional locking.
func (*Group) Limit ¶
Limit returns g and a function that starts each task passed to it in g, allowing no more than n tasks to be active concurrently. If n ≤ 0, the start function is equivalent to g.Go, which enforces no limit.
The limiting mechanism is optional, and the underlying group is not restricted. A call to the start function will block until a slot is available, but calling g.Go directly will add a task unconditionally and will not take up a limiter slot.
Example ¶
var p peakValue
g, start := taskgroup.New(nil).Limit(4)
for i := 0; i < 100; i++ {
start(func() error {
p.inc()
defer p.dec()
time.Sleep(1 * time.Microsecond)
return nil
})
}
g.Wait()
fmt.Printf("Max active ≤ 4: %v\n", p.max <= 4)
Output: Max active ≤ 4: true
func (*Group) Wait ¶
Wait blocks until all the goroutines currently active in the group have returned, and all reported errors have been delivered to the callback. It returns the first non-nil error reported by any of the goroutines in the group and not filtered by an ErrorFunc.
As with sync.WaitGroup, new tasks can be added to g during a call to Wait only if the group contains at least one active task when Wait is called and continuously thereafter until the last concurrent call to g.Go returns.
Wait may be called from at most one goroutine at a time. After Wait has returned, the group is ready for reuse.
type Single ¶ added in v0.4.0
type Single[T any] struct { // contains filtered or unexported fields }
A single manages a single background goroutine. The task is started when the value is first created, and the caller can use the Wait method to block until it has exited.
Example ¶
package main
import (
"fmt"
"io"
"log"
"time"
"github.com/creachadair/taskgroup"
)
type slowReader struct {
n int
d time.Duration
}
func (s *slowReader) Read(data []byte) (int, error) {
if s.n == 0 {
return 0, io.EOF
}
time.Sleep(s.d)
nr := min(len(data), s.n)
s.n -= nr
for i := 0; i < nr; i++ {
data[i] = 'x'
}
return nr, nil
}
func main() {
// A fake reader to simulate a slow file read.
// 2500 bytes and each read takes 50ms.
sr := &slowReader{2500, 50 * time.Millisecond}
var data []byte
// Start a task to read te "file" in the background.
fmt.Println("start")
s := taskgroup.Go(func() error {
var err error
data, err = io.ReadAll(sr)
return err
})
fmt.Println("work, work")
if err := s.Wait(); err != nil {
log.Fatalf("Read failed: %v", err)
}
fmt.Println("done")
fmt.Println(len(data), "bytes")
}
Output: start work, work done 2500 bytes
func Call ¶ added in v0.6.0
Call starts task in a new goroutine. The caller must call Wait to wait for the task to return and collect its result.