All Benchmarks

Go Benchmark

Channel vs Mutex

Sharing a value by communicating vs communicating by sharing.

channelmutexsyncconcurrencycontention

"Don't communicate by sharing memory; share memory by communicating" is Go's most famous proverb — but channels are not free. This benchmark moves an int64 between two points using a buffered channel (send + receive) and compares it against a mutex-guarded variable (locked write + locked read), so both variants pay exactly two synchronization points per operation. The "Serial" tab shows the uncontended baseline cost of each primitive; the "Parallel" tab shows what happens under contention from multiple goroutines. Channels buy you ownership transfer, select, and cancellation semantics — this chart shows what that abstraction costs when all you need is to protect a value.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/channel-vs-mutex
Compare atCPUs

1 CPU

Parallel

Fastest

Mutex

18.44 ns/op

Slowest

Buffered Channel

27.67 ns/op · 1.5x slower

Serial

Fastest

Mutex

14.33 ns/op

Slowest

Buffered Channel

24.01 ns/op · 1.7x slower

32 CPUs

Parallel

Fastest

Mutex

69.79 ns/op

Slowest

Buffered Channel

94 ns/op

Serial

Fastest

Mutex

14.97 ns/op

Slowest

Buffered Channel

24.04 ns/op · 1.6x slower

Performance Comparison (lower is better)
CPU:

Mutex #

Fastest (Parallel)Fastest (Serial)

Writes the value into a shared variable under a sync.Mutex, then reads it back under the same lock. In the uncontended fast path, lock and unlock are each a single atomic operation with no allocations, making this the cheapest way to protect plain shared state when you don't need channel semantics.

Performance (lower is better)
CPU:
func BenchmarkMutex_serial(b *testing.B) {
	var mu sync.Mutex
	var shared int64
	var total int64
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// One locked write and one locked read — the same two
		// synchronization points as a channel send plus receive.
		mu.Lock()
		shared = int64(i)
		mu.Unlock()

		mu.Lock()
		total += shared
		mu.Unlock()
	}
	sink.Add(total)
}

func BenchmarkMutex_parallel(b *testing.B) {
	var mu sync.Mutex
	var shared int64

	b.RunParallel(func(pb *testing.PB) {
		var total int64
		i := int64(0)
		for pb.Next() {
			mu.Lock()
			shared = i
			mu.Unlock()

			mu.Lock()
			total += shared
			mu.Unlock()
			i++
		}
		sink.Add(total)
	})
}
1 CPU

Parallel

1.5xfaster(50%)thanBuffered Channel

Serial

1.7xfaster(68%)thanBuffered Channel
32 CPUs

Parallel

1.3xfaster(35%)thanBuffered Channel

Serial

1.6xfaster(61%)thanBuffered Channel

Buffered Channel #

Slowest (Parallel)Slowest (Serial)

Sends the value into a chan int64 with buffer size 1 and receives it back. Every send and receive goes through the runtime's channel machinery: an internal mutex, copying the element, and potentially parking/waking goroutines under contention. That machinery is what enables select, timeouts, and clean handoff semantics.

Performance (lower is better)
CPU:
func BenchmarkBufferedChannel_serial(b *testing.B) {
	ch := make(chan int64, 1)
	var total int64
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Hand the value over via the channel, then take it back.
		ch <- int64(i)
		total += <-ch
	}
	sink.Add(total)
}

func BenchmarkBufferedChannel_parallel(b *testing.B) {
	ch := make(chan int64, 1)

	b.RunParallel(func(pb *testing.PB) {
		var total int64
		i := int64(0)
		for pb.Next() {
			ch <- i
			total += <-ch
			i++
		}
		sink.Add(total)
	})
}
1 CPU

Parallel

1.5xslower(50%)thanMutex

Serial

1.7xslower(68%)thanMutex
32 CPUs

Parallel

1.3xslower(35%)thanMutex

Serial

1.6xslower(61%)thanMutex

Contributors