Go Concurrency and Parallelism
January 22, 2025 · 3 min read
If you have any questions, feel free to comment below. Click the block can copy the code.
And if you think it's helpful to you, just click on the ads which can support this site. Thanks!
Process and Thread #
The process is the program execution process in the operating system, and it is an independent unit for resource allocation and scheduling.
The thread is the execution entity of the process, the basic unit for CPU scheduling and distribution, and it is a smaller unit that can run independently.
A process can create and cancel multiple threads; multiple threads in the same process can execute concurrently.
Concurrency and Parallelism #
Concurrency is multiple threads running on the same CPU core.
Parallelism is multiple threads running on multiple CPU cores.
Goroutine #
A thread can run multiple goroutines, and a goroutine is a lightweight thread, which is managed by the Go runtime, independent stack space, shared heap space, scheduling controlled by the user, essentially similar to user-level threads. The cost of routine is KB level which is much lower than thread MB level.
goroutine is just a super “thread pool” implemented by the official. The 4~5KB stack memory occupied by each instance and the greatly reduced creation and destruction overhead due to the implementation mechanism are the fundamental reason for Go’s high concurrency.
A goroutine must correspond to a function, you just need to add the go keyword before the function.
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
go hello() // use goroutine
fmt.Println("main goroutine done!")
}
// output:
// main goroutine done!
// why? the main goroutine life cycle is over, the program will exit.
// this only for the main goroutine, so for the other goroutine, if return, the sub-routine can still running.
Use sync.WaitGroup to wait for goroutines instead of guessing their completion time with time.Sleep. Call Add before starting each goroutine, Done when it finishes, and Wait to block until the counter reaches zero.
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine done, add -1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // add a goroutine
go hello(i)
}
wg.Wait() // wait all goroutine done
}
Communicating with channels #
Go encourages sharing data by communicating between goroutines rather than having multiple goroutines update shared memory directly. Channels carry values of a specific type:
unbuffered := make(chan int) // A send waits for a receiver.
buffered := make(chan int, 4) // Sends wait only when the buffer is full.
_, _ = unbuffered, buffered
An unbuffered channel synchronizes the sender and receiver: a send cannot complete until a receiver is ready. A buffered channel can hold up to its capacity; sends block when it is full, and receives block when it is empty. This makes channels useful for coordinating producers and consumers, but a buffer alone does not make unrelated shared-memory accesses safe.
For example, one goroutine sends numbers, another squares them, and the main goroutine receives the results. Each sender closes only the channel it owns after sending its last value:
package main
import "fmt"
func main() {
numbers := make(chan int)
squares := make(chan int)
go func() {
defer close(numbers)
for i := 0; i < 10; i++ {
numbers <- i
}
}()
go func() {
defer close(squares)
for n := range numbers {
squares <- n * n
}
}()
for square := range squares {
fmt.Println(square)
}
}
Protecting shared data #
When goroutines must access the same mutable state, protect it with sync.Mutex or another synchronization mechanism. Otherwise, concurrent reads and writes can cause a data race:
var mu sync.Mutex
var count int
// In each goroutine:
mu.Lock()
count++
mu.Unlock()
A WaitGroup waits for work to finish; it does not protect shared variables. Run go test -race ./... to detect data races in tests.
Related readings
If you want to follow my updates, or have a coffee chat with me, feel free to connect with me: