Here's a program that I've been using to explore Go concurrency. If you want to play along at home, you'll need to compile and run it locally; the Playground won't suffice.
package main import ( "fmt" "runtime" "syscall" ) type Response struct { Received int Calculated int Handler int Tid int } func Listener(me int, in chan int, out chan Response, done chan int) { for val := range in { out <- Response{val, runCalc(me), me, syscall.Gettid()} } done <- me } func runCalc(num int) int { zz := 1 for ii := 0 ; ii < 100000000 ; ii++ { zz += ii % num } return zz } func main() { fmt.Println("Main running on thread ", syscall.Gettid(), " numCPU = ", runtime.NumCPU()) chSend := make(chan int, 100) chRecv := make(chan Response) chDone := make(chan int) listenerCount:= 8 for ii := 1 ; ii <= listenerCount ; ii++ { go Listener(ii, chSend, chRecv, chDone) } messageCount := 100 for ii := 0 ; ii < messageCount ; ii++ { chSend <- ii } close(chSend) for listenerCount > 0 { select { case data := <- chRecv : fmt.Println("Received ", data.Received, ",", data.Calculated, " from ", data.Handler, " on thread ", data.Tid) messageCount-- case lnum := <- chDone : fmt.Println("Received DONE from ", lnum) listenerCount-- } } fmt.Println("Main done, outstanding messages = ", messageCount) }
The short description of this program is that it kicks off a bunch of goroutines, then sends them CPU-intensive work. My goal in writing it was to explore thread affinity and communication patterns as the amount of work increased. Imagine my surprise when I saw the following output from my 4 core CPU:
go run multi_listener.go Main running on thread 27638 , numCPU = 8 Received 0 , 1 from 1 on thread 27640 Received 1 , 1 from 1 on thread 27640 Received 2 , 50000001 from 2 on thread 27640 Received 3 , 100000000 from 3 on thread 27640 Received 4 , 150000001 from 4 on thread 27640 Received 5 , 200000001 from 5 on thread 27640 Received 6 , 249999997 from 6 on thread 27640 Received 7 , 299999996 from 7 on thread 27640 Received 8 , 350000001 from 8 on thread 27640 Received 9 , 1 from 1 on thread 27640 Received 10 , 1 from 1 on thread 27640 Received 11 , 50000001 from 2 on thread 27640 Received 12 , 100000000 from 3 on thread 27640 Received 13 , 150000001 from 4 on thread 27640 Received 14 , 200000001 from 5 on thread 27640
The thread ID is's always the same! And top
confirmed that this wasn't a lie: one
core was consuming 100% of the CPU, while the others were idle. It took some Googling to discover
the GOMAXPROCS
environment variable:
experiments, 505> export GOMAXPROCS=4 experiments, 506> go run multi_listener.go Main running on thread 27674 , numCPU = 8 Received 2 , 350000001 from 8 on thread 27677 Received 0 , 299999996 from 7 on thread 27678 Received 1 , 1 from 1 on thread 27674 Received 3 , 350000001 from 8 on thread 27677 Received 4 , 50000001 from 2 on thread 27679 Received 5 , 299999996 from 7 on thread 27678 Received 6 , 200000001 from 5 on thread 27674 Received 7 , 249999997 from 6 on thread 27677 Received 8 , 100000000 from 3 on thread 27679 Received 9 , 1 from 1 on thread 27678 Received 10 , 200000001 from 5 on thread 27674 Received 11 , 150000001 from 4 on thread 27677
This variable is documented in the runtime package docs, and also in the (28 page) FAQ. It's not mentioned in the Go Tour or tutorial.
I'm a bit taken aback that it's even necessary, however the comment that goes along with the associated
runtime
method gives a hit: “This call will go away when the scheduler improves.”
As of Go 1.2, the behavior remains, one of the quirks of using a young framework.
No comments:
Post a Comment