Go goroutine management, WaitGroup and Context

There are two classic way to handle concurrency in go, WaitGroup and Context

What is WaitGroup

WaitGroup wait for multiple goroutines to finish

content_copy
func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("1st task done")
		wg.Done()
	}()
	go func() {
		time.Sleep(2*time.Second)
		fmt.Println("2nd task done")
		wg.Done()
	}()
	wg.Wait()
	fmt.Println("Finish")
}

The program only finishes when the 2 goroutine finished. Otherwise, it will wait for it.
This is useful when we want a program to wait for all tasks to finish.

However, sometimes we want to actively cancel a goroutine instead of wait for it to finish. An example can be monitoring. We want to exit monitoring instead of wait for it to finish (it will never finish). We can use channel for this usecase.

Channel

we can use channel + select to repeatedly checking on a global variable to notify the end of a process

content_copy
func main() {
	stop := make(chan bool)

	go func() {
		for {
			select {
			case <-stop:
				fmt.Println("monitoring finish, exit")
				return
			default:
				fmt.Println("gorouting monitoring")
				time.Sleep(2 * time.Second)
			}
		}
	}()

	time.Sleep(10 * time.Second)
	fmt.Println("notify end of monitoring")
	stop<- true
  // check if goroutine finished
	time.Sleep(5 * time.Second)

}

Here we defined a stop channel to notify the goroutine.
In goroutine, we use  select to determine if stop can receive a value. If we receive a value, we can stop monitoring. Otherwise, execute the default logic.

What if we have a goroutine within a goroutine. Channel + select will be too complicated to write up and handle this situation. We need context.

Context

Context helps to track the state of goroutine

content_copy
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("monitoring finish, exit")
				return
			default:
				fmt.Println("gorouting monitoring")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)

	time.Sleep(10 * time.Second)
	fmt.Println("notify end of monitoring")
	cancel()
  // check if goroutine finished
	time.Sleep(5 * time.Second)

}

context.Background() returns an empty Context which acts as a root of the context tree.
Then we use context.WithCancel(parent) function to create a cancellable subContext, then pass it as a parameter to goroutine. We are now able to use subContext to track the state of goroutine

in goroutine, we use select to receive the return of <-ctx.Done() to decide if we should terminate goroutine.

We send the cancel signal through cancel function. The cancel function was created when we use context.WithCancel(parent)

Controlling multiple goroutine using Context

content_copy
func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx,"monitor 1")
	go watch(ctx,"monitor 2")
	go watch(ctx,"monitor 3")

	time.Sleep(10 * time.Second)
	fmt.Println("notify exit monitoring")
	cancel()
  // check if goroutine finished
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name,"exit monitoring")
			return
		default:
			fmt.Println(name,"goroutine monitoring")
			time.Sleep(2 * time.Second)
		}
	}
}

We use 3 gorouting monitoring and track them using a Context.
When we signal cancel, all goroutines will terminate.

Context interface

content_copy
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key interface{}) interface{}
}
content_copy
var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
  • Background mainly used in main function, initialization or testing as the root Context
  • TODO usually used when we don't know what Context to used

They are essentially a emptyCtx struct

content_copy
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}

emptyCtx Context mostly return nil or empty.

Context with-fuctions

content_copy
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

WithValue example

content_copy
var key string="name"

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//assign value
	valueCtx:=context.WithValue(ctx,key,"monitoring 1")
	go watch(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("notify exit monitoring")
	cancel()
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			//retrieve value
			fmt.Println(ctx.Value(key),"exit monitoring")
			return
		default:
			//retrieve value
			fmt.Println(ctx.Value(key),"goroutine monitoring")
			time.Sleep(2 * time.Second)
		}
	}
}

Context principals

  1. Do not store Context in struct, pass as a parameter
  2. When pass Context as a parameter, put it in the 1st parameter
  3. Do not pass nil as a Context. If we are not sure what to use, use context.TODO
  4. only store necessary value in Context Value
  5. Context is threadsafe

Comments