// lesson: communication-as-scheduling

Communication as Scheduling

It's tempting to picture a channel as a little thread-safe queue. Look at runtime/chan.go and a different picture emerges: a channel is a scheduling data structure. The hchan struct holds a ring buffer, yes — but also two wait-lists of parked goroutines, recvq and sendq.

The interesting path is the rendezvous. When a goroutine sends on a channel where a receiver is already parked, the runtime doesn't touch the buffer at all: send copies the value directly into the parked receiver's stack frame (a rare case of one goroutine writing another's stack), then calls goready on it. Communication and scheduling are literally the same operation — moving a value moves a goroutine from _Gwaiting to _Grunnable. The woken receiver is even placed in the P's runnext slot, so it typically runs next, keeping producer–consumer pairs hot in cache.

One channel operation is special: close. A send wakes one receiver; close walks recvq and readies every parked goroutine (and makes all future receives complete immediately with the zero value). That makes close the runtime's broadcast primitive — the idiomatic one-shot event. context.Done() is exactly this: a channel nobody ever sends on, closed once to wake all listeners, forever.

Combine broadcast-on-close with "write the value before closing, read it after receiving" (close is a release/acquire pair under the memory model — the write happens before any receive that observes the close), and you get a future: a cell that starts empty, is resolved exactly once, and wakes every waiter — past and future — with the same value.

// The one-shot broadcast pattern:
done := make(chan struct{})
var val string

// resolver (exactly once):
val = "ready"  // write...
close(done)    // ...then publish

// any number of waiters, before or after resolution:
<-done
use(val) // guaranteed to see "ready"

Waiting must also be cancellable — a Get that can outlive the caller's interest is a goroutine leak factory. select is how a goroutine parks on several events at once (one sudog per case, first ready wins), which makes context integration one arm each.

Futures

25 pts

Implement a generic, resolve-once future:

type Future[T any] struct { /* ... */ }

// NewFuture returns an unresolved future and its resolve function.
func NewFuture[T any]() (*Future[T], func(T))

// Get blocks until the future is resolved or ctx is done.
func (f *Future[T]) Get(ctx context.Context) (T, error)

Required semantics:

  • Get before resolution blocks (parks — no polling loops).
  • Any number of Get callers, before or after resolution, all receive the same resolved value with a nil error.
  • If ctx is cancelled while waiting, Get returns the zero value of T and ctx.Err().
  • Calling the resolve function a second time panics (with any message). Guard it with an atomic compare-and-swap, not a mutex — you built locks already; this one only needs a once-flag.

Build it exactly like the lesson's pattern: value field + chan struct{} closed on resolve; Get is a two-arm select.

Log in to submit a solution and earn points.