I am creating a go program that is intended to run long term and listen for work. When it receives a request, it runs the work on a process queue.
I am new to golang and systems programming, so my question is this: should I spin up the process queue (with it's multiple idle worker threads) at the program launch (they will just sit there until work comes in) or should I spin them up when work arrives and shut them down when finished?
I am unclear as to the overall system impact multiple idle threads will have, but I am assuming since they are idle there will be no impact until work arrives. That being said, I want to make sure my program is a "good neighbor" and as efficient as possible.
--EDIT--
To clarify, the "process pool" is a group of worker go routines waiting for work on a channel. Should they be started/stopped when work arrives, or started when the program launches and left waiting until work comes in?
First of all you can't create a thread using standard Go library. In Go universe you should use goroutines which are so called green threads.
Usually you shouldn't spawn "reusable" goroutines. They are cheap to create so create them on demand as work job arrives and finish (return from goroutine) as soon as work is completed.
Also don't hesitate to create nested goroutines. In general spawn them like crazy if you feel you should do something in concurrent manner and don't try to reuse them as it makes no sense.
Pattern with tasks queue and waiting workers is common in Go. Goroutines are cheap, but order of execution is nondetermined. So if you want your system behavior to be predictable, you better would control workers rendezvous with main routine thru unbuffered channels requested in a loop or somehow else. Otherwise some of them can be spawned but remain idle which is legal.
There is very little cost either way. goroutines don't require a separate OS thread and consume practically no resources while blocking on a channel receive, but also cost very little to spin up, so there's no great reason to leave them open either.
My code rarely uses worker pools. Generally my producer will spawn a goroutine for every unit of work it produces and hands it off directly along with a response channel, then spawns a "listener" that does some formatting for the work output and pipes all the responses back to the main thread. A common pattern for me looks like:
func Foo(input []interface{}) resp chan interface{} {
var wg sync.WaitGroup
resp := make(chan interface{})
listen := make(chan interface{})
theWork := makeWork(input)
// do work
for _, unitOfWork := range theWork {
wg.Add(1)
go func() {
// doWork has signature:
// func doWork(w interface{}, ch chan interface{})
doWork(unitOfWork, listen)
wg.Done()
}()
}
// format the output of listen chan and send to resp chan
// then close resp chan so main can continue
go func() {
for r := range listen {
resp <- doFormatting(r)
}
close(resp)
}()
// close listen chan after work is done
go func() {
wg.Wait()
close(listen)
}()
return resp
}
Then my main function passes it some input and listens on the response channel
func main() {
loremipsum := []string{"foo", "bar", "spam", "eggs"}
response := Foo(loremipsum)
for output := range response {
fmt.Println(output)
}
}