Block Profiling
Block profiling in Go helps identify synchronization bottlenecks by measuring how long goroutines spend blocked on synchronization primitives like mutexes, channels, and select statements. This comprehensive guide covers advanced block profiling techniques for optimizing concurrent Go applications.
Introduction to Block Profiling
Block profiling tracks:
- Mutex contention - Time spent waiting for mutex locks
- Channel operations - Blocking on channel send/receive
- Select statements - Waiting in select cases
- Semaphore operations - Blocking on semaphores
- Condition variables - Waiting on sync.Cond
Enabling Block Profiling
package main
import (
"context"
"fmt"
"net/http"
_ "net/http/pprof"
"runtime"
"sync"
"time"
)
func init() {
// Enable block profiling
runtime.SetBlockProfileRate(1)
}
// BlockProfiler provides comprehensive block profiling capabilities
type BlockProfiler struct {
samplingRate int
profileDuration time.Duration
outputPath string
serverAddr string
collecting bool
mu sync.RWMutex
}
func NewBlockProfiler() *BlockProfiler {
return &BlockProfiler{
samplingRate: 1, // Sample every nanosecond of blocking
profileDuration: 30 * time.Second,
outputPath: "block.prof",
serverAddr: ":6060",
}
}
func (bp *BlockProfiler) SetSamplingRate(rate int) {
bp.mu.Lock()
defer bp.mu.Unlock()
bp.samplingRate = rate
runtime.SetBlockProfileRate(rate)
}
func (bp *BlockProfiler) StartProfiling() error {
bp.mu.Lock()
defer bp.mu.Unlock()
if bp.collecting {
return fmt.Errorf("profiling already in progress")
}
// Set block profile rate
runtime.SetBlockProfileRate(bp.samplingRate)
bp.collecting = true
fmt.Printf("Block profiling started (rate: %d, duration: %v)\n",
bp.samplingRate, bp.profileDuration)
return nil
}
func (bp *BlockProfiler) StopProfiling() error {
bp.mu.Lock()
defer bp.mu.Unlock()
if !bp.collecting {
return fmt.Errorf("no profiling in progress")
}
// Disable block profiling
runtime.SetBlockProfileRate(0)
bp.collecting = false
fmt.Println("Block profiling stopped")
return nil
}
func (bp *BlockProfiler) StartServer() {
fmt.Printf("Starting pprof server on %s\n", bp.serverAddr)
fmt.Printf("Block profile: http://localhost%s/debug/pprof/block\n", bp.serverAddr)
log.Fatal(http.ListenAndServe(bp.serverAddr, nil))
}
func (bp *BlockProfiler) CollectProfile(duration time.Duration) error {
if err := bp.StartProfiling(); err != nil {
return err
}
time.Sleep(duration)
return bp.StopProfiling()
}
func demonstrateBasicBlockProfiling() {
fmt.Println("=== BASIC BLOCK PROFILING ===")
profiler := NewBlockProfiler()
profiler.SetSamplingRate(1) // Sample every blocking event
// Start profiling server in background
go profiler.StartServer()
// Start profiling
profiler.StartProfiling()
// Create some blocking scenarios
var wg sync.WaitGroup
// Mutex contention
var mu sync.Mutex
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 10; j++ {
mu.Lock()
time.Sleep(10 * time.Millisecond) // Hold lock
mu.Unlock()
time.Sleep(5 * time.Millisecond)
}
}(i)
}
wg.Wait()
// Channel blocking
ch := make(chan int)
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
ch <- id*10 + j
}
}(i)
}
go func() {
for i := 0; i < 15; i++ {
<-ch
time.Sleep(20 * time.Millisecond)
}
}()
wg.Wait()
time.Sleep(2 * time.Second)
profiler.StopProfiling()
fmt.Println("Block profiling completed. Check pprof server for results.")
}
Advanced Block Analysis Techniques
Mutex Contention Analysis
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
)
// MutexContentionAnalyzer tracks mutex contention patterns
type MutexContentionAnalyzer struct {
mutexes map[string]*MutexStats
mu sync.RWMutex
totalBlocked int64
totalTime int64
}
type MutexStats struct {
Name string
LockCount int64
BlockCount int64
BlockTime int64
ContentionRate float64
}
func NewMutexContentionAnalyzer() *MutexContentionAnalyzer {
return &MutexContentionAnalyzer{
mutexes: make(map[string]*MutexStats),
}
}
// TrackedMutex wraps sync.Mutex with contention tracking
type TrackedMutex struct {
sync.Mutex
name string
analyzer *MutexContentionAnalyzer
locks int64
blocks int64
blockTime int64
}
func (mca *MutexContentionAnalyzer) NewTrackedMutex(name string) *TrackedMutex {
tm := &TrackedMutex{
name: name,
analyzer: mca,
}
mca.mu.Lock()
mca.mutexes[name] = &MutexStats{Name: name}
mca.mu.Unlock()
return tm
}
func (tm *TrackedMutex) Lock() {
start := time.Now()
// Try to lock without blocking first
if tm.Mutex.TryLock() {
atomic.AddInt64(&tm.locks, 1)
return
}
// If we can't get it immediately, it's contended
atomic.AddInt64(&tm.blocks, 1)
tm.Mutex.Lock()
blockDuration := time.Since(start)
atomic.AddInt64(&tm.blockTime, int64(blockDuration))
atomic.AddInt64(&tm.locks, 1)
// Update global stats
atomic.AddInt64(&tm.analyzer.totalBlocked, 1)
atomic.AddInt64(&tm.analyzer.totalTime, int64(blockDuration))
}
func (tm *TrackedMutex) TryLock() bool {
if tm.Mutex.TryLock() {
atomic.AddInt64(&tm.locks, 1)
return true
}
return false
}
func (tm *TrackedMutex) GetStats() MutexStats {
locks := atomic.LoadInt64(&tm.locks)
blocks := atomic.LoadInt64(&tm.blocks)
blockTime := atomic.LoadInt64(&tm.blockTime)
var contentionRate float64
if locks > 0 {
contentionRate = float64(blocks) / float64(locks) * 100
}
return MutexStats{
Name: tm.name,
LockCount: locks,
BlockCount: blocks,
BlockTime: blockTime,
ContentionRate: contentionRate,
}
}
func (mca *MutexContentionAnalyzer) GetReport() ContentionReport {
mca.mu.RLock()
defer mca.mu.RUnlock()
var mutexStats []MutexStats
totalContentions := int64(0)
totalTime := time.Duration(0)
for _, tm := range mca.mutexes {
// Get current stats from TrackedMutex
// This is a simplified version - in practice you'd maintain references
stats := MutexStats{
Name: tm.Name,
LockCount: 0, // Would be populated from actual TrackedMutex
BlockCount: 0,
BlockTime: 0,
ContentionRate: 0,
}
mutexStats = append(mutexStats, stats)
totalContentions += stats.BlockCount
totalTime += time.Duration(stats.BlockTime)
}
return ContentionReport{
TotalMutexes: len(mutexStats),
TotalContentions: totalContentions,
TotalBlockTime: totalTime,
MutexStats: mutexStats,
}
}
type ContentionReport struct {
TotalMutexes int
TotalContentions int64
TotalBlockTime time.Duration
MutexStats []MutexStats
}
func (cr ContentionReport) String() string {
result := fmt.Sprintf(`Mutex Contention Report:
Total Mutexes: %d
Total Contentions: %d
Total Block Time: %v`,
cr.TotalMutexes,
cr.TotalContentions,
cr.TotalBlockTime)
if len(cr.MutexStats) > 0 {
result += "\n\nPer-Mutex Statistics:"
for _, stats := range cr.MutexStats {
result += fmt.Sprintf("\n %s:", stats.Name)
result += fmt.Sprintf("\n Locks: %d", stats.LockCount)
result += fmt.Sprintf("\n Blocks: %d", stats.BlockCount)
result += fmt.Sprintf("\n Block Time: %v", time.Duration(stats.BlockTime))
result += fmt.Sprintf("\n Contention Rate: %.2f%%", stats.ContentionRate)
}
}
return result
}
// Demonstration of different contention patterns
func demonstrateContentionPatterns() {
fmt.Println("\n=== MUTEX CONTENTION PATTERNS ===")
analyzer := NewMutexContentionAnalyzer()
// High contention scenario
highContentionMutex := analyzer.NewTrackedMutex("high_contention")
var wg sync.WaitGroup
numGoroutines := 10
operationsPerGoroutine := 100
fmt.Println("Testing high contention scenario...")
start := time.Now()
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
highContentionMutex.Lock()
// Simulate work while holding lock
time.Sleep(time.Microsecond * 100)
highContentionMutex.Unlock()
// Small gap between operations
time.Sleep(time.Microsecond * 10)
}
}(i)
}
wg.Wait()
highContentionTime := time.Since(start)
// Low contention scenario
lowContentionMutex := analyzer.NewTrackedMutex("low_contention")
fmt.Println("Testing low contention scenario...")
start = time.Now()
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ {
lowContentionMutex.Lock()
// Very brief work
time.Sleep(time.Microsecond * 1)
lowContentionMutex.Unlock()
// Longer gap between operations
time.Sleep(time.Microsecond * 500)
}
}(i)
}
wg.Wait()
lowContentionTime := time.Since(start)
// Show results
fmt.Printf("\nHigh contention stats:\n%s\n", highContentionMutex.GetStats())
fmt.Printf("Low contention stats:\n%s\n", lowContentionMutex.GetStats())
fmt.Printf("\nTiming comparison:\n")
fmt.Printf(" High contention: %v\n", highContentionTime)
fmt.Printf(" Low contention: %v\n", lowContentionTime)
fmt.Printf(" Ratio: %.2fx slower\n", float64(highContentionTime)/float64(lowContentionTime))
}
func (ms MutexStats) String() string {
return fmt.Sprintf(` Name: %s
Locks: %d
Blocks: %d
Block Time: %v
Contention Rate: %.2f%%`,
ms.Name,
ms.LockCount,
ms.BlockCount,
time.Duration(ms.BlockTime),
ms.ContentionRate)
}
Channel Blocking Analysis
package main
import (
"context"
"fmt"
"sync"
"sync/atomic"
"time"
)
// ChannelBlockingAnalyzer tracks channel operation blocking
type ChannelBlockingAnalyzer struct {
channels map[string]*ChannelStats
mu sync.RWMutex
}
type ChannelStats struct {
Name string
SendOps int64
RecvOps int64
SendBlocks int64
RecvBlocks int64
SendTime int64
RecvTime int64
}
func NewChannelBlockingAnalyzer() *ChannelBlockingAnalyzer {
return &ChannelBlockingAnalyzer{
channels: make(map[string]*ChannelStats),
}
}
// TrackedChannel wraps a channel with blocking analysis
type TrackedChannel struct {
ch chan interface{}
name string
analyzer *ChannelBlockingAnalyzer
stats *ChannelStats
}
func (cba *ChannelBlockingAnalyzer) NewTrackedChannel(name string, buffer int) *TrackedChannel {
stats := &ChannelStats{Name: name}
cba.mu.Lock()
cba.channels[name] = stats
cba.mu.Unlock()
return &TrackedChannel{
ch: make(chan interface{}, buffer),
name: name,
analyzer: cba,
stats: stats,
}
}
func (tc *TrackedChannel) Send(value interface{}) {
start := time.Now()
select {
case tc.ch <- value:
// Non-blocking send
atomic.AddInt64(&tc.stats.SendOps, 1)
return
default:
// Would block
atomic.AddInt64(&tc.stats.SendBlocks, 1)
}
// Perform blocking send
tc.ch <- value
duration := time.Since(start)
atomic.AddInt64(&tc.stats.SendTime, int64(duration))
atomic.AddInt64(&tc.stats.SendOps, 1)
}
func (tc *TrackedChannel) Receive() interface{} {
start := time.Now()
select {
case value := <-tc.ch:
// Non-blocking receive
atomic.AddInt64(&tc.stats.RecvOps, 1)
return value
default:
// Would block
atomic.AddInt64(&tc.stats.RecvBlocks, 1)
}
// Perform blocking receive
value := <-tc.ch
duration := time.Since(start)
atomic.AddInt64(&tc.stats.RecvTime, int64(duration))
atomic.AddInt64(&tc.stats.RecvOps, 1)
return value
}
func (tc *TrackedChannel) TryReceive() (interface{}, bool) {
select {
case value := <-tc.ch:
atomic.AddInt64(&tc.stats.RecvOps, 1)
return value, true
default:
return nil, false
}
}
func (tc *TrackedChannel) Close() {
close(tc.ch)
}
func (tc *TrackedChannel) GetStats() ChannelStats {
return ChannelStats{
Name: tc.stats.Name,
SendOps: atomic.LoadInt64(&tc.stats.SendOps),
RecvOps: atomic.LoadInt64(&tc.stats.RecvOps),
SendBlocks: atomic.LoadInt64(&tc.stats.SendBlocks),
RecvBlocks: atomic.LoadInt64(&tc.stats.RecvBlocks),
SendTime: atomic.LoadInt64(&tc.stats.SendTime),
RecvTime: atomic.LoadInt64(&tc.stats.RecvTime),
}
}
func (cs ChannelStats) String() string {
sendBlockRate := float64(0)
if cs.SendOps > 0 {
sendBlockRate = float64(cs.SendBlocks) / float64(cs.SendOps) * 100
}
recvBlockRate := float64(0)
if cs.RecvOps > 0 {
recvBlockRate = float64(cs.RecvBlocks) / float64(cs.RecvOps) * 100
}
return fmt.Sprintf(`Channel: %s
Send Operations: %d (%.1f%% blocked)
Recv Operations: %d (%.1f%% blocked)
Send Block Time: %v
Recv Block Time: %v`,
cs.Name,
cs.SendOps, sendBlockRate,
cs.RecvOps, recvBlockRate,
time.Duration(cs.SendTime),
time.Duration(cs.RecvTime))
}
func (cba *ChannelBlockingAnalyzer) GetReport() ChannelReport {
cba.mu.RLock()
defer cba.mu.RUnlock()
var channelStats []ChannelStats
totalSendBlocks := int64(0)
totalRecvBlocks := int64(0)
totalSendTime := time.Duration(0)
totalRecvTime := time.Duration(0)
for _, stats := range cba.channels {
channelStats = append(channelStats, *stats)
totalSendBlocks += stats.SendBlocks
totalRecvBlocks += stats.RecvBlocks
totalSendTime += time.Duration(stats.SendTime)
totalRecvTime += time.Duration(stats.RecvTime)
}
return ChannelReport{
TotalChannels: len(channelStats),
TotalSendBlocks: totalSendBlocks,
TotalRecvBlocks: totalRecvBlocks,
TotalSendTime: totalSendTime,
TotalRecvTime: totalRecvTime,
ChannelStats: channelStats,
}
}
type ChannelReport struct {
TotalChannels int
TotalSendBlocks int64
TotalRecvBlocks int64
TotalSendTime time.Duration
TotalRecvTime time.Duration
ChannelStats []ChannelStats
}
func (cr ChannelReport) String() string {
result := fmt.Sprintf(`Channel Blocking Report:
Total Channels: %d
Total Send Blocks: %d
Total Recv Blocks: %d
Total Send Block Time: %v
Total Recv Block Time: %v`,
cr.TotalChannels,
cr.TotalSendBlocks,
cr.TotalRecvBlocks,
cr.TotalSendTime,
cr.TotalRecvTime)
if len(cr.ChannelStats) > 0 {
result += "\n\nPer-Channel Statistics:"
for _, stats := range cr.ChannelStats {
result += "\n" + stats.String()
}
}
return result
}
func demonstrateChannelBlocking() {
fmt.Println("\n=== CHANNEL BLOCKING ANALYSIS ===")
analyzer := NewChannelBlockingAnalyzer()
// Test unbuffered channel (high blocking)
unbuffered := analyzer.NewTrackedChannel("unbuffered", 0)
// Test buffered channel (lower blocking)
buffered := analyzer.NewTrackedChannel("buffered", 10)
var wg sync.WaitGroup
// Producer-consumer pattern with unbuffered channel
fmt.Println("Testing unbuffered channel...")
// Slow consumer
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 20; i++ {
unbuffered.Receive()
time.Sleep(10 * time.Millisecond) // Slow consumption
}
}()
// Fast producer
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 20; i++ {
unbuffered.Send(i)
time.Sleep(2 * time.Millisecond) // Fast production
}
}()
wg.Wait()
// Producer-consumer pattern with buffered channel
fmt.Println("Testing buffered channel...")
// Same slow consumer
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 20; i++ {
buffered.Receive()
time.Sleep(10 * time.Millisecond)
}
}()
// Same fast producer
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 20; i++ {
buffered.Send(i)
time.Sleep(2 * time.Millisecond)
}
}()
wg.Wait()
// Show results
fmt.Printf("\nUnbuffered channel stats:\n%s\n", unbuffered.GetStats())
fmt.Printf("Buffered channel stats:\n%s\n", buffered.GetStats())
report := analyzer.GetReport()
fmt.Printf("\n%s\n", report)
}
Select Statement Analysis
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
)
// SelectAnalyzer tracks select statement blocking patterns
type SelectAnalyzer struct {
selects map[string]*SelectStats
mu sync.RWMutex
}
type SelectStats struct {
Name string
Executions int64
CaseHits map[int]int64
Timeouts int64
Defaults int64
BlockTime int64
AvgBlockTime time.Duration
}
func NewSelectAnalyzer() *SelectAnalyzer {
return &SelectAnalyzer{
selects: make(map[string]*SelectStats),
}
}
// TrackedSelect provides instrumented select operations
type TrackedSelect struct {
name string
analyzer *SelectAnalyzer
stats *SelectStats
}
func (sa *SelectAnalyzer) NewTrackedSelect(name string, numCases int) *TrackedSelect {
stats := &SelectStats{
Name: name,
CaseHits: make(map[int]int64),
}
for i := 0; i < numCases; i++ {
stats.CaseHits[i] = 0
}
sa.mu.Lock()
sa.selects[name] = stats
sa.mu.Unlock()
return &TrackedSelect{
name: name,
analyzer: sa,
stats: stats,
}
}
func (ts *TrackedSelect) ExecuteSelect(selectFunc func() (int, bool)) {
start := time.Now()
caseIndex, blocked := selectFunc()
duration := time.Since(start)
atomic.AddInt64(&ts.stats.Executions, 1)
if blocked {
atomic.AddInt64(&ts.stats.BlockTime, int64(duration))
}
if caseIndex >= 0 {
ts.stats.CaseHits[caseIndex]++
} else if caseIndex == -1 {
atomic.AddInt64(&ts.stats.Defaults, 1)
} else if caseIndex == -2 {
atomic.AddInt64(&ts.stats.Timeouts, 1)
}
}
func (ts *TrackedSelect) GetStats() SelectStats {
executions := atomic.LoadInt64(&ts.stats.Executions)
blockTime := atomic.LoadInt64(&ts.stats.BlockTime)
var avgBlockTime time.Duration
if executions > 0 {
avgBlockTime = time.Duration(blockTime / executions)
}
return SelectStats{
Name: ts.stats.Name,
Executions: executions,
CaseHits: ts.stats.CaseHits,
Timeouts: atomic.LoadInt64(&ts.stats.Timeouts),
Defaults: atomic.LoadInt64(&ts.stats.Defaults),
BlockTime: blockTime,
AvgBlockTime: avgBlockTime,
}
}
func (ss SelectStats) String() string {
result := fmt.Sprintf(`Select: %s
Executions: %d
Average Block Time: %v
Total Block Time: %v
Timeouts: %d
Defaults: %d`,
ss.Name,
ss.Executions,
ss.AvgBlockTime,
time.Duration(ss.BlockTime),
ss.Timeouts,
ss.Defaults)
if len(ss.CaseHits) > 0 {
result += "\n Case Distribution:"
for i, hits := range ss.CaseHits {
percentage := float64(0)
if ss.Executions > 0 {
percentage = float64(hits) / float64(ss.Executions) * 100
}
result += fmt.Sprintf("\n Case %d: %d (%.1f%%)", i, hits, percentage)
}
}
return result
}
// Complex select scenarios for testing
func demonstrateSelectAnalysis() {
fmt.Println("\n=== SELECT STATEMENT ANALYSIS ===")
analyzer := NewSelectAnalyzer()
// Test multi-channel select
multiChannelSelect := analyzer.NewTrackedSelect("multi_channel", 3)
ch1 := make(chan int, 5)
ch2 := make(chan string, 5)
ch3 := make(chan bool, 5)
var wg sync.WaitGroup
// Producer for different channels at different rates
wg.Add(3)
go func() {
defer wg.Done()
for i := 0; i < 50; i++ {
ch1 <- i
time.Sleep(10 * time.Millisecond)
}
close(ch1)
}()
go func() {
defer wg.Done()
for i := 0; i < 30; i++ {
ch2 <- fmt.Sprintf("msg_%d", i)
time.Sleep(15 * time.Millisecond)
}
close(ch2)
}()
go func() {
defer wg.Done()
for i := 0; i < 20; i++ {
ch3 <- i%2 == 0
time.Sleep(25 * time.Millisecond)
}
close(ch3)
}()
// Consumer using tracked select
wg.Add(1)
go func() {
defer wg.Done()
for {
multiChannelSelect.ExecuteSelect(func() (int, bool) {
start := time.Now()
select {
case val, ok := <-ch1:
if !ok {
return -1, time.Since(start) > time.Microsecond
}
_ = val
return 0, time.Since(start) > time.Microsecond
case val, ok := <-ch2:
if !ok {
return -1, time.Since(start) > time.Microsecond
}
_ = val
return 1, time.Since(start) > time.Microsecond
case val, ok := <-ch3:
if !ok {
return -1, time.Since(start) > time.Microsecond
}
_ = val
return 2, time.Since(start) > time.Microsecond
default:
return -1, false // Default case
}
})
// Check if all channels are closed
if isClosed(ch1) && isClosed(ch2) && isClosed(ch3) {
break
}
time.Sleep(1 * time.Millisecond)
}
}()
wg.Wait()
// Test timeout select
timeoutSelect := analyzer.NewTrackedSelect("timeout", 2)
slowCh := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
timeoutSelect.ExecuteSelect(func() (int, bool) {
start := time.Now()
select {
case val := <-slowCh:
_ = val
return 0, time.Since(start) > time.Microsecond
case <-time.After(50 * time.Millisecond):
return -2, true // Timeout case
}
})
}
}()
// Occasionally send to slow channel
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 3; i++ {
time.Sleep(time.Duration(100+rand.Intn(100)) * time.Millisecond)
select {
case slowCh <- i:
default:
}
}
close(slowCh)
}()
wg.Wait()
// Show results
fmt.Printf("Multi-channel select stats:\n%s\n", multiChannelSelect.GetStats())
fmt.Printf("Timeout select stats:\n%s\n", timeoutSelect.GetStats())
}
func isClosed(ch chan int) bool {
select {
case <-ch:
return true
default:
return false
}
}
Optimization Strategies
Lock-Free Alternatives
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
// Compare lock-based vs lock-free implementations
func compareLockVsLockFree() {
fmt.Println("\n=== LOCK VS LOCK-FREE COMPARISON ===")
iterations := 1000000
numGoroutines := 10
// Lock-based counter
var lockCounter int64
var mu sync.Mutex
var wg sync.WaitGroup
fmt.Println("Testing lock-based counter...")
start := time.Now()
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations/numGoroutines; j++ {
mu.Lock()
lockCounter++
mu.Unlock()
}
}()
}
wg.Wait()
lockTime := time.Since(start)
// Lock-free counter
var atomicCounter int64
fmt.Println("Testing lock-free counter...")
start = time.Now()
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations/numGoroutines; j++ {
atomic.AddInt64(&atomicCounter, 1)
}
}()
}
wg.Wait()
atomicTime := time.Since(start)
fmt.Printf("Results:\n")
fmt.Printf(" Lock-based: %d operations in %v\n", lockCounter, lockTime)
fmt.Printf(" Lock-free: %d operations in %v\n", atomicCounter, atomicTime)
fmt.Printf(" Speedup: %.2fx\n", float64(lockTime)/float64(atomicTime))
}
Channel Buffer Optimization
package main
import (
"fmt"
"sync"
"time"
)
func optimizeChannelBuffers() {
fmt.Println("\n=== CHANNEL BUFFER OPTIMIZATION ===")
testSizes := []int{0, 1, 10, 100, 1000}
numMessages := 10000
for _, bufferSize := range testSizes {
fmt.Printf("Testing buffer size: %d\n", bufferSize)
ch := make(chan int, bufferSize)
var wg sync.WaitGroup
start := time.Now()
// Producer
wg.Add(1)
go func() {
defer wg.Done()
defer close(ch)
for i := 0; i < numMessages; i++ {
ch <- i
}
}()
// Consumer
wg.Add(1)
go func() {
defer wg.Done()
for range ch {
// Simulate processing
time.Sleep(time.Microsecond)
}
}()
wg.Wait()
duration := time.Since(start)
fmt.Printf(" Duration: %v\n", duration)
}
}
Best Practices for Block Profiling
1. Enable Profiling Strategically
func enableBlockProfiling() {
// Set appropriate sampling rate
runtime.SetBlockProfileRate(1) // Sample every blocking event
// Or sample less frequently for production
// runtime.SetBlockProfileRate(1000) // Sample every 1µs of blocking
}
2. Analyze Profile Data
# Collect block profile
go tool pprof http://localhost:6060/debug/pprof/block
# Analyze in web interface
go tool pprof -http=:8080 block.prof
# Find top blocking operations
(pprof) top10
# Show blocking call tree
(pprof) tree
# Focus on specific functions
(pprof) focus sync.Mutex
3. Optimize Based on Results
// Replace highly contended mutexes with:
// 1. Lock-free operations
var counter int64
atomic.AddInt64(&counter, 1)
// 2. Sharded locks
type ShardedCounter struct {
shards []struct {
mu sync.Mutex
count int64
}
}
// 3. Channels for coordination
func coordinateWithChannels() {
requests := make(chan Request, 100)
responses := make(chan Response, 100)
// Process requests without locks
go func() {
for req := range requests {
resp := process(req)
responses <- resp
}
}()
}
Next Steps
- Learn Trace Analysis techniques
- Study Concurrency Optimization
- Explore Performance Benchmarking
- Master Production Monitoring
Summary
Block profiling in Go helps identify:
- Mutex contention - High lock contention points
- Channel bottlenecks - Blocking channel operations
- Select inefficiencies - Suboptimal select patterns
- Synchronization overhead - Cost of coordination
- Optimization opportunities - Lock-free alternatives
Use block profiling to build more efficient concurrent applications with reduced synchronization overhead.