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
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
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
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
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
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
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 ContextTODO
usually used when we don't know what Context to used
They are essentially a emptyCtx
struct
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
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
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
- Do not store Context in struct, pass as a parameter
- When pass Context as a parameter, put it in the 1st parameter
- Do not pass nil as a Context. If we are not sure what to use, use context.TODO
- only store necessary value in Context Value
- Context is threadsafe
Comments
Post a Comment