Skip to content

Instantly share code, notes, and snippets.

@joshlf
Created October 2, 2016 17:05
Show Gist options
  • Save joshlf/c3e3a4f45f531bcbb7648d3d3bc3c361 to your computer and use it in GitHub Desktop.
Save joshlf/c3e3a4f45f531bcbb7648d3d3bc3c361 to your computer and use it in GitHub Desktop.
A demonstration of a self-cleaning worker
// This is an example of a self-cleaning worker. A common pattern in Go
// is to have a type which, when created with NewX, spawns one or more
// goroutines in the background to perform work. It is usually the
// caller's responsibility to call Close() or Stop() in order to shut
// these workers down and not leak goroutines and the memory resources
// that the goroutines have references to.
//
// The idea behind a self-cleaning worker is to leverage the runtime's
// ability to set finalizers on objects in order to detect when an object
// with a still-live worker goroutine has gone out of scope. The type
// is split into two types - a *doerHelper, which does the heavy lifting,
// and which the worker goroutines have a reference to, and a *doer, which
// is the interface from the package consumer's perspective. A *doer
// contains a *doerHelper, and methods called on the *doer are simply
// wrappers around the same methods on the *doerHelper. The reason for this
// setup is that none of the woker goroutines have references to the *doer,
// so when the package consumer's copy goes out of scope, the entire object
// can be garbage collected. A finalizer is set on the *doer which, when
// executed, can clean up the worker goroutines if they haven't been clenaed
// up already (the example implementation is simplistic and assumes that
// they haven't been cleaned up because it's just for demonstration purposes).
//
// This program implements an example of this pattern which provides two
// creation functions: newDoer, and newSelfCleaningDoer. The former does
// the normal thing, and the latter implements the self-cleaning pattern.
// First, a number of doers are created with newDoer. Second, a number of
// doers are created with newSelfCleaningDoer. The number of goroutines is
// continually printed. Finally, the GC is invoked in order to trigger the
// finalizers, and the main goroutine sleeps in order to give the worker
// goroutines a chance to clean themselves up. In a normal long-lived
// program, this would happen more slowly over time. Forcing the GC and
// then sleeping is just for demonstration purposes - otherwise, the program
// would exit before any of this had a chance to happen.
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
const rounds = 1024 * 64
fmt.Println("Phase 1")
for i := 0; i < rounds; i++ {
newDoer()
if i%1024 == 0 {
fmt.Println(runtime.NumGoroutine())
}
}
fmt.Printf("Goroutines after phase 1: %v\n\n", runtime.NumGoroutine())
fmt.Println("Phase 2")
for i := 0; i < rounds; i++ {
newSelfCleaningDoer()
if i%1024 == 0 {
fmt.Println(runtime.NumGoroutine())
}
}
fmt.Printf("Goroutines after phase 2: %v\n\n", runtime.NumGoroutine())
fmt.Println("Forcing GC...")
fmt.Println("Sleeping to give daemons a chance to exit...")
runtime.GC()
time.Sleep(time.Second)
fmt.Printf("Final goroutines: %v\n", runtime.NumGoroutine())
}
type doer struct {
dh *doerHelper
}
func newDoer() *doer {
d := &doer{&doerHelper{}}
d.dh.wg.Add(1)
go d.dh.daemon()
return d
}
func newSelfCleaningDoer() *doer {
d := &doer{&doerHelper{}}
f := func(d *doer) {
d.dh.Stop()
}
runtime.SetFinalizer(d, f)
d.dh.wg.Add(1)
go d.dh.daemon()
return d
}
type doerHelper struct {
wg sync.WaitGroup
}
func (d *doerHelper) daemon() {
d.wg.Wait()
}
func (d *doerHelper) Stop() {
d.wg.Done()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment