Skip to content

Acquire / Release — Tasks

Hands-on exercises for practicing acquire/release publication patterns in Go. Solve each on paper, then in code, then verify with go test -race.

Task 1: Safe Flag Publication (Junior)

Problem: Implement a Flag type. One goroutine sets the flag; many goroutines wait for it to be set. Use atomic.Bool.

Skeleton:

type Flag struct {
    set atomic.Bool
}

func (f *Flag) Set()        { f.set.Store(true) }
func (f *Flag) IsSet() bool { return f.set.Load() }
func (f *Flag) Wait() {
    for !f.set.Load() {
        runtime.Gosched()
    }
}

Test:

func TestFlag(t *testing.T) {
    var f Flag
    done := make(chan struct{})
    go func() {
        time.Sleep(10 * time.Millisecond)
        f.Set()
        close(done)
    }()
    f.Wait()
    if !f.IsSet() {
        t.Error("flag not set")
    }
    <-done
}

Run with go test -race.


Task 2: Lazy Singleton (Junior)

Problem: Implement a thread-safe lazy singleton for a *Config struct using sync.Once.

Skeleton:

var (
    once   sync.Once
    config *Config
)

func GetConfig() *Config {
    once.Do(func() {
        config = loadConfig()
    })
    return config
}

Test: verify 10 concurrent calls all return the same pointer.


Task 3: Safe Pointer Publication (Junior-Middle)

Problem: Implement a Container[T] that holds a pointer set once, then read many times.

Skeleton:

type Container[T any] struct {
    val atomic.Pointer[T]
}

func (c *Container[T]) Set(v *T) { c.val.Store(v) }
func (c *Container[T]) Get() *T  { return c.val.Load() }

Test: confirm that after Set, any concurrent Get sees the new pointer (not stale nil).


Task 4: Read-Mostly Cache (Middle)

Problem: Implement a string-to-int cache where reads are wait-free and writes are infrequent.

Skeleton:

type Cache struct {
    data atomic.Pointer[map[string]int]
    mu   sync.Mutex
}

func (c *Cache) Get(k string) (int, bool) {
    m := c.data.Load()
    if m == nil { return 0, false }
    v, ok := (*m)[k]
    return v, ok
}

func (c *Cache) Set(k string, v int) {
    c.mu.Lock()
    defer c.mu.Unlock()
    old := c.data.Load()
    n := map[string]int{}
    if old != nil {
        for kk, vv := range *old {
            n[kk] = vv
        }
    }
    n[k] = v
    c.data.Store(&n)
}

Test: stress test with 16 goroutines reading and 4 writing for 1 second. Verify no races.


Task 5: Worker Pool with Graceful Shutdown (Middle)

Problem: Implement a pool of N worker goroutines that processes jobs from a channel. Shutdown waits for all workers to finish current jobs.

Skeleton:

type Pool struct {
    jobs chan Job
    wg   sync.WaitGroup
    stop chan struct{}
    once sync.Once
}

func NewPool(n int, jobCap int) *Pool {
    p := &Pool{
        jobs: make(chan Job, jobCap),
        stop: make(chan struct{}),
    }
    for i := 0; i < n; i++ {
        p.wg.Add(1)
        go p.worker()
    }
    return p
}

func (p *Pool) worker() {
    defer p.wg.Done()
    for {
        select {
        case <-p.stop:
            return
        case j := <-p.jobs:
            process(j)
        }
    }
}

func (p *Pool) Submit(j Job) bool {
    select {
    case p.jobs <- j:
        return true
    case <-p.stop:
        return false
    }
}

func (p *Pool) Stop() {
    p.once.Do(func() { close(p.stop) })
    p.wg.Wait()
}

Test: submit 1000 jobs across 4 producers, then Stop. Verify all jobs processed.


Task 6: Single-Flight Cache (Middle-Senior)

Problem: Implement a cache where concurrent identical requests for the same key collapse into a single upstream fetch.

Skeleton:

type SingleFlight struct {
    mu      sync.Mutex
    pending map[string]*pendingCall
    cache   map[string]string
}

type pendingCall struct {
    done chan struct{}
    val  string
    err  error
}

func (sf *SingleFlight) Get(k string, fetch func() (string, error)) (string, error) {
    sf.mu.Lock()
    if v, ok := sf.cache[k]; ok {
        sf.mu.Unlock()
        return v, nil
    }
    if p, ok := sf.pending[k]; ok {
        sf.mu.Unlock()
        <-p.done
        return p.val, p.err
    }
    p := &pendingCall{done: make(chan struct{})}
    if sf.pending == nil { sf.pending = map[string]*pendingCall{} }
    sf.pending[k] = p
    sf.mu.Unlock()

    p.val, p.err = fetch()

    sf.mu.Lock()
    if sf.cache == nil { sf.cache = map[string]string{} }
    if p.err == nil {
        sf.cache[k] = p.val
    }
    delete(sf.pending, k)
    sf.mu.Unlock()

    close(p.done)
    return p.val, p.err
}

Test: 100 concurrent calls for the same key; fetch should run exactly once.


Task 7: Atomic Counter Snapshot (Senior)

Problem: Track a counter that can be atomically reset on read.

Skeleton:

type ResettableCounter struct {
    n atomic.Int64
}

func (c *ResettableCounter) Inc() { c.n.Add(1) }

func (c *ResettableCounter) SnapshotAndReset() int64 {
    return c.n.Swap(0)
}

Test: verify no Inc calls are lost during concurrent SnapshotAndReset.


Task 8: Promise/Future (Senior)

Problem: Implement a write-once, read-many "promise" with cancellation.

Skeleton:

type Promise[T any] struct {
    val   T
    err   error
    done  chan struct{}
    once  sync.Once
}

func NewPromise[T any]() *Promise[T] {
    return &Promise[T]{done: make(chan struct{})}
}

func (p *Promise[T]) Resolve(v T) {
    p.once.Do(func() {
        p.val = v
        close(p.done)
    })
}

func (p *Promise[T]) Reject(err error) {
    p.once.Do(func() {
        p.err = err
        close(p.done)
    })
}

func (p *Promise[T]) Await(ctx context.Context) (T, error) {
    select {
    case <-p.done:
        return p.val, p.err
    case <-ctx.Done():
        var zero T
        return zero, ctx.Err()
    }
}

Test: spawn N goroutines awaiting the same promise; verify all get the same value.


Task 9: Lock-Free Stack (Senior)

Problem: Implement a Treiber stack with Push and Pop using atomic.Pointer + CAS.

Skeleton:

type Stack[T any] struct {
    head atomic.Pointer[node[T]]
}

type node[T any] struct {
    val  T
    next *node[T]
}

func (s *Stack[T]) Push(v T) {
    n := &node[T]{val: v}
    for {
        n.next = s.head.Load()
        if s.head.CompareAndSwap(n.next, n) {
            return
        }
    }
}

func (s *Stack[T]) Pop() (T, bool) {
    for {
        top := s.head.Load()
        if top == nil {
            var zero T
            return zero, false
        }
        if s.head.CompareAndSwap(top, top.next) {
            return top.val, true
        }
    }
}

Test: stress with many concurrent Push and Pop; verify no values lost or duplicated.


Task 10: Sharded Counter (Senior)

Problem: Build a counter that scales to many CPUs without contention.

Skeleton:

type ShardedCounter struct {
    shards []paddedInt64
}

type paddedInt64 struct {
    n atomic.Int64
    _ [56]byte
}

func NewShardedCounter() *ShardedCounter {
    return &ShardedCounter{shards: make([]paddedInt64, 64)}
}

func (c *ShardedCounter) Inc() {
    idx := getProcID() % 64
    c.shards[idx].n.Add(1)
}

func (c *ShardedCounter) Sum() int64 {
    var s int64
    for i := range c.shards {
        s += c.shards[i].n.Load()
    }
    return s
}

Test: benchmark uncontended Inc against atomic.Int64.Add at GOMAXPROCS=16.


Task 11: Seqlock (Senior-Professional)

Problem: Implement a seqlock for fast reads with occasional writes.

Skeleton:

type Seqlock struct {
    seq atomic.Uint64
    mu  sync.Mutex
    x, y atomic.Int64
}

func (s *Seqlock) Write(xv, yv int64) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.seq.Add(1)
    s.x.Store(xv)
    s.y.Store(yv)
    s.seq.Add(1)
}

func (s *Seqlock) Read() (int64, int64) {
    for {
        v1 := s.seq.Load()
        if v1%2 != 0 { continue }
        x := s.x.Load()
        y := s.y.Load()
        v2 := s.seq.Load()
        if v1 == v2 {
            return x, y
        }
    }
}

Test: stress with 1 writer, 8 readers; verify readers see consistent pairs.


Task 12: Event Subscriber (Professional)

Problem: Implement a publish-subscribe bus with lock-free subscribe/unsubscribe and publish.

Skeleton:

type Bus[T any] struct {
    subs atomic.Pointer[[]chan<- T]
    mu   sync.Mutex
}

func (b *Bus[T]) Subscribe(ch chan<- T) {
    b.mu.Lock()
    defer b.mu.Unlock()
    old := b.subs.Load()
    var n []chan<- T
    if old != nil { n = append(n, *old...) }
    n = append(n, ch)
    b.subs.Store(&n)
}

func (b *Bus[T]) Publish(v T) {
    subs := b.subs.Load()
    if subs == nil { return }
    for _, ch := range *subs {
        select {
        case ch <- v:
        default:
        }
    }
}

Test: subscribe many channels; publish; verify each receives.


Task 13: Rate Limiter (Professional)

Problem: Token-bucket rate limiter with no contention on read path.

Skeleton: see professional.md Appendix DX.

Test: measure ops/sec at high concurrency; should remain wait-free.


Task 14: Concurrent LRU Cache (Professional)

Problem: Implement a sharded LRU cache.

Skeleton: use a fixed number of shards, each with its own mutex, map, and LRU list.

Test: verify correctness under stress, then benchmark scaling.


Task 15: MPSC Queue (Professional)

Problem: Implement Vyukov's MPSC queue.

Skeleton: see professional.md Appendix BL.

Test: verify FIFO order, wait-free producers, lock-free consumer.


Test Harness Template

For all tasks, use this template:

package main_test

import (
    "sync"
    "testing"
)

func TestStress(t *testing.T) {
    // setup
    var wg sync.WaitGroup
    const N = 64
    for i := 0; i < N; i++ {
        wg.Add(1)
        go func(seed int) {
            defer wg.Done()
            for j := 0; j < 10000; j++ {
                // exercise the structure
            }
        }(i)
    }
    wg.Wait()
    // assert invariants
}

Run with go test -race -count=10.


Conclusion

These tasks cover the main acquire/release patterns in Go. Solve them; benchmark them; reason about their happens-before chains. By the end, the patterns will be second nature.

End of tasks.md.