Mastering Go Concurrency: Goroutines, Scheduler, Race Detection & Channels
This article explains Go's concurrency model, detailing how goroutines are scheduled onto logical processors, how to create and run them, handle race conditions with atomic operations, mutexes, and the race detector, and share data safely using unbuffered and buffered channels with practical code examples.
1. Using Goroutine to Run Programs
1. Go Concurrency and Parallelism
Go's concurrency capability allows a function to run independently of others. When a goroutine is created, the function becomes a work unit scheduled by the runtime scheduler onto an available logical processor. The scheduler manages all goroutines, allocates execution time, and binds OS threads to logical processors.
Manages all created goroutines and assigns them execution time.
Binds OS threads to language runtime logical processors.
The OS schedules OS threads on physical CPUs, while the Go runtime schedules goroutine on logical processors. Three roles are involved:
M: the OS thread, the actual kernel thread.
P: the logical processor, providing scheduling context for goroutines on an M.
G: the goroutine, with its own stack and instruction pointer, scheduled by a P.
Each P maintains a global runqueue of ready goroutines. When go func starts a goroutine, it is appended to the runqueue; at the next scheduling point, P picks a goroutine from the runqueue to execute.
If an OS thread M blocks (e.g., a goroutine performs a blocking system call), the P can bind to another OS thread M, allowing other goroutines to continue. The scheduler ensures enough threads; the default limit is 10,000 threads, adjustable via runtime/debug.SetMaxThreads.
Go can achieve concurrency on a single logical processor P; to achieve parallelism, multiple logical processors are required. The scheduler distributes goroutines evenly across logical processors, which run on separate OS threads when multiple physical CPUs are available.
2. Creating Goroutine
Use the go keyword to launch a goroutine and let all goroutines execute.
//example1.go
package main
import (
"runtime"
"sync"
"fmt"
)
var (
wg sync.WaitGroup
)
func main() {
// Assign one logical processor to the scheduler
runtime.GOMAXPROCS(1)
wg.Add(2)
fmt.Printf("Begin Coroutines
")
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
fmt.Printf("Waiting To Finish
")
wg.Wait()
}This program uses runtime.GOMAXPROCS(1) to allocate one logical processor; the two goroutines are scheduled on that processor. Output shows the first goroutine finishes before the second.
Begin Coroutines
Waiting To Finish
A B C ... Z a b c ... zTo run the two goroutines in parallel, set two logical processors:
// Set to 2 logical processors
runtime.GOMAXPROCS(2)Running with two processors interleaves the output of uppercase and lowercase letters.
Begin Coroutines
Waiting To Finish
A B C ... Z a b c ... zIf only one logical processor is available, you can force cooperative scheduling with runtime.Gosched() at desired points.
//example2.go
package main
import (
"runtime"
"sync"
"fmt"
)
var (
wg sync.WaitGroup
)
func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
fmt.Printf("Begin Coroutines
")
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
if char == 'k' {
runtime.Gosched()
}
fmt.Printf("%c ", char)
}
}
}()
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
if char == 'K' {
runtime.Gosched()
}
fmt.Printf("%c ", char)
}
}
}()
fmt.Printf("Waiting To Finish
")
wg.Wait()
}Both goroutines yield when the character reaches k/K, resulting in alternating output.
Begin Coroutines
Waiting To Finish
A B C ... K L ... Z a b c ... k ... z2. Handling Race Conditions
Concurrent programs may have unsynchronized access to shared resources, leading to race conditions when multiple goroutines read and write the same variable.
//example3.go
package main
import (
"sync"
"runtime"
"fmt"
)
var (
counter int64
wg sync.WaitGroup
)
func addCount() {
defer wg.Done()
for count := 0; count < 2; count++ {
value := counter
runtime.Gosched()
value++
counter = value
}
}
func main() {
wg.Add(2)
go addCount()
go addCount()
wg.Wait()
fmt.Printf("counter: %d
", counter)
}Running this may produce counter: 4 or counter: 2 due to races.
Solutions:
Use atomic functions (e.g., atomic.AddInt64).
Use a mutex to protect the critical section.
Use channels for synchronization.
1. Detecting Races
Go provides a race detector. Compile with -race and run the program:
go build -race example4.go ./example4
The detector reports the lines where the race occurs.
2. Using Atomic Functions
Atomic operations safely modify integers without locks.
//example5.go
package main
import (
"sync"
"runtime"
"fmt"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func addCount() {
defer wg.Done()
for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1)
runtime.Gosched()
}
}
func main() {
wg.Add(2)
go addCount()
go addCount()
wg.Wait()
fmt.Printf("counter: %d
", counter)
}Output is always counter: 4.
3. Using Mutex
//example6.go
package main
import (
"sync"
"runtime"
"fmt"
)
var (
counter int
wg sync.WaitGroup
mutex sync.Mutex
)
func addCount() {
defer wg.Done()
for count := 0; count < 2; count++ {
mutex.Lock()
value := counter
runtime.Gosched()
value++
counter = value
mutex.Unlock()
}
}
func main() {
wg.Add(2)
go addCount()
go addCount()
wg.Wait()
fmt.Printf("counter: %d
", counter)
}The mutex ensures only one goroutine updates counter at a time, yielding counter: 4.
3. Sharing Data with Channels
Go follows the CSP model, using channels ( chan) to pass messages between goroutines instead of locking shared data.
unbuffered := make(chan int) // unbuffered channel for int
buffered := make(chan string, 10) // buffered channel for string
buffered <- "hello world"
value := <-bufferedUnbuffered channels are synchronous: a send blocks until a receive is ready, and vice versa. Buffered channels allow storing multiple values before a receive.
1. Unbuffered Channels
Example simulating a tennis match where two players exchange a ball via an unbuffered channel:
//example7.go
package main
import (
"sync"
"fmt"
"math/rand"
"time"
)
var wg sync.WaitGroup
func player(name string, court chan int) {
defer wg.Done()
for {
ball, ok := <-court
if !ok {
fmt.Printf("Player %s Won
", name)
return
}
n := rand.Intn(100)
if n%13 == 0 {
fmt.Printf("Player %s Missed
", name)
close(court)
return
}
fmt.Printf("Player %s Hit %d
", name, ball)
ball++
court <- ball
}
}
func main() {
rand.Seed(time.Now().Unix())
court := make(chan int)
wg.Add(2)
go player("candy", court)
go player("luffic", court)
court <- 1
wg.Wait()
}Output shows the ball being hit back and forth until a miss occurs.
2. Buffered Channels
Buffered channels store values until they are received; sending blocks only when the buffer is full, and receiving blocks only when the buffer is empty. Closing a channel prevents further sends but allows remaining values to be received.
Summary
Goroutine execution is managed by logical processors, each with its own OS thread and runqueue.
Multiple goroutines can run concurrently on one logical processor; parallelism requires multiple logical processors.
Use the go keyword to create goroutines.
Race conditions arise when goroutines concurrently access shared resources.
Mutexes or atomic functions can prevent races.
Channels provide a preferred way to share data safely in Go.
Unbuffered channels are synchronous; buffered channels are not.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
