t.Parallel is kinda neat
To the credit of the Go language team, I've barely given t.Parallel a thought beyond "add this at the top and tests go brrr...". Today I was reading a test that did some work before calling t.Parallel and I wondered how that affected parallel test execution. In particular, my mental model was that parallelisation was a binary decision, either the test is or isn't running in parallel. However, this obviously doesn't make sense, because the call to t.Parallel only occurs within the body of the test itself, so absent some AST parsing, it must happen during execution.
First I asked an LLM, which gave me about one tenth of the picture, that t.Parallel continues the rest of the test execution within a goroutine, but after that it made increasingly more unbelievable claims, like that if a parallel test ran first and then began executing in a goroutine, that a serial test might start running in parallel as a result of test ordering. ¯\_(ツ)_/¯
As far as I can divine without diving too deeply into the code, ignoring subtests, and GOMAXPROCS, the gist of it is:
- The test runner loops over each
TestXXXfunction, starting a goroutine and waiting for a signal before proceeding, typically from a test completing. - When a test calls
t.Parallelit uses the same channel to signal the test runner to continue, and then immediately blocks, waiting for a signal that all sequential tests have completed. - After the last sequential test has completed, it signals the aforementioned channel, and all waiting tests continue in parallel.
The interesting thing about this is that it means parallelisable tests can block others before t.Parallel is called. You can see how this plays out in the verbose logs for this test below, with TestParallel beginning to RUN, and then PAUSING before CONTINUING when TestSerial has completed.
func TestParallel(t *testing.T) {
fmt.Println("parallel starting")
t.Parallel()
fmt.Println("parallel continuing")
}
func TestSerial(t *testing.T) {
fmt.Println("serial starting")
}
=== RUN TestParallel
parallel starting
=== PAUSE TestParallel
=== RUN TestSerial
serial starting
--- PASS: TestSerial (0.00s)
=== CONT TestParallel
parallel continuing
--- PASS: TestParallel (0.00s)
PASS
All that said, I've been writing Go since 2015 and I never gave this much thought before, so in that regard, I think t.Parallel is kinda neat!