I have a process with a number of stages that need to be completed in sequence. Each stage is largely parallelisable, involving looping over a large data structure and processing each item independently, and collating the results into the data structure used by the next stage.
My approach is like the following:
var wg sync.WaitGroup
var control_routine sync.WaitGroup
stage_complete = make(chan bool)
stage_one_channel := make(chan map[typeFoo][]typeBar, 16)
stage_two_channel := make(chan map[typeBaz][]typeQux, 16)
stage_one_results := make(map[typeFoo][]typeBar)
stage_two_results := make(map[typeBaz][]typeQux)
// A routine to receive results as they arrive from each stage
go func() {
for {
select {
case new_stage_one_results := <- stage_one_channel:
// merge new_stage_one_results into stage_one_results...
case new_stage_two_results := <- stage_two_channel:
// merge new_stage_two_results into stage_two_results...
...
case stay_alive := <-stage_complete:
control_routine.Done()
if !stay_alive {
return
}
}
}
}()
// Begin stage One
control_routine.Add(1)
for i, some_data := range my_data {
wg.Add(1)
go func(i, datum){
defer wg.Done()
some_stage_one_results = make(map[typeFoo][]typeBar)
// do some heavy lifting with some_data which populates some_stage_one_results...
stage_one_channel <- some_stage_one_results
}()
}
wg.Wait()
stage_complete <- true
control_routine.Wait()
// End stage one
// More stages much like the first...
// Finally send the control routine a terminal signal and wait for it to finish
stage_complete <- false
control_routine.Wait()
This basically seems to work, but I need to be sure of the following assumption (which some behaviour is causing me to doubt).
Can I be sure that all go routines in stage N will completely finish and the data sent through stage_N_channel will all be processed in the control routine before the stage_complete channel is read from?
What I'm seeing is that sometimes, it looks like one or two results from a stage go missing, as if the control routine were moving on before processing them. Adding a one second sleep to the main routine after wg.Wait()
, seems to help but I'm not sure is this is right.
Is there anything else unusual or potentially dangerous about this pattern? Is there another pattern I should consider using?
I finally settled on a solution where each stage resembles the following.
for i, some_data := range my_data {
wg.Add(1)
go func(i, some_data){
// stage logic
...
stage_one_channel <- some_stage_one_results
}(i, some_data)
}
go func() {
wg.Wait()
close(stage_one_channel)
}()
for new_stage_one_data := <- stage_one_channel {
// integrate received data
...
wg.Done()
}