DevPrep
  • Interview Prep
  • Projects
  • Resources
  • Pricing
  • About Us
Submit Question
DevPrep
  • Pricing
  • About Us
Submit Question

Practice

  • JavaScript
  • DSA
  • Machine Coding
  • System Design

Resources

  • Learning Tracks
  • Articles
  • Roadmaps
  • Compare Concepts
  • Glossary
  • Developer Tools
  • All Questions

Company

  • About
  • Pricing

Legal

  • Privacy Policy
  • Terms of Service
DevPrep

© 2026 DevPrep. All rights reserved.

← Back to Questions
HardTheory

When to Use Mutex vs Channels for Synchronization

23 views

Problem Statement

A Staff Engineer at Stripe asks: "We have a high-frequency payment counter that increments millions of times per second. Should we use a Mutex, Channels, or Atomics? Explain your reasoning and benchmark the difference."

The Go Proverb

"Share memory by communicating, don't communicate by sharing memory."

But this doesn't mean channels are always the right choice.

Decision Framework

Use CaseBest ChoiceReason
Simple counteratomic.AddInt64Hardware instruction, fastest
Protecting complex statesync.MutexClear ownership semantics
Coordinating goroutinesChannelsCommunication pattern
Read-heavy, write-raresync.RWMutexMultiple concurrent readers

Benchmark Implementation

package main

import (
    "sync"
    "sync/atomic"
    "testing"
)

// Mutex approach
type MutexCounter struct {
    mu    sync.Mutex
    count int64
}

func (c *MutexCounter) Inc() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}

// Atomic approach
type AtomicCounter struct {
    count int64
}

func (c *AtomicCounter) Inc() {
    atomic.AddInt64(&c.count, 1)
}

// Channel approach
type ChannelCounter struct {
    ch    chan struct{}
    count int64
}

func NewChannelCounter() *ChannelCounter {
    c := &ChannelCounter{ch: make(chan struct{}, 100)}
    go func() {
        for range c.ch {
            c.count++
        }
    }()
    return c
}

func (c *ChannelCounter) Inc() {
    c.ch <- struct{}{}
}

// Benchmarks
func BenchmarkMutexCounter(b *testing.B) {
    c := &MutexCounter{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            c.Inc()
        }
    })
}

func BenchmarkAtomicCounter(b *testing.B) {
    c := &AtomicCounter{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            c.Inc()
        }
    })
}

Typical Results

BenchmarkAtomicCounter-8    200000000    8.5 ns/op
BenchmarkMutexCounter-8      50000000   32.0 ns/op
BenchmarkChannelCounter-8    10000000  150.0 ns/op

Conclusion

For the payment counter scenario: Use atomic operations. They're ~4x faster than Mutex and ~18x faster than channels for simple increments.

Follow-up Questions

  1. When would channels be the better choice despite being slower?
  2. What is "False Sharing" and how does it affect atomic counters?
  3. How do you implement a lock-free data structure?

Sample Test Cases

Case 1
Input
shared counter, multiple goroutines
Expected Output
Use Mutex for simple state protection
Case 2
Input
producer-consumer pattern
Expected Output
Use channels for communication

No solutions yet

Be the first to share a solution for this question.

Comments (0)

Sign in to leave a comment.

No comments yet. Be the first to comment.

Stats

Views
23
Likes
0
Solutions
0
Comments
0

Category

Backend Engineering

Languages

Go