6. Concurrency Primitives

CSP Channel

midconcurrencydata-structures

Go's channels are the synchronization primitive goroutines use to pass values to each other, with a maxsize that changes their behavior fundamentally: 0 means unbuffered/synchronous, >0 means buffered. Since this exercise has no real concurrency, "blocking" is modeled explicitly: an operation that couldn't complete immediately returns a status saying it WOULD have blocked, instead of actually blocking.

Buffered (maxsize > 0): send() succeeds immediately if the buffer isn't full yet.

Unbuffered (maxsize == 0): an unbuffered channel has NO storage. send() only succeeds if a receiver is ALREADY WAITING for a value, so the two operations "rendezvous" directly. recv() on an empty unbuffered channel doesn't just fail -- it registers itself as waiting (receivers_waiting), so a SUBSEQUENT send() can find it and complete.

Closed (the part most people get wrong): implement close(), and make send/recv respect it:

  • send() on a closed channel is a hard error -- raise ChannelClosedError.
  • recv() on a closed channel still returns whatever's left in the buffer first; only once it's genuinely drained does it report (False, None).
  • Critically: recv() on a closed, drained, UNBUFFERED channel must NOT register itself as a waiting receiver -- nothing will ever be sent on a closed channel again, so waiting for it would wait forever.

The missing link: closed isn't just "permanently empty"

It's tempting to treat a closed channel as "acts empty forever," but that breaks two real things at once. First, a closed channel can still have buffered values worth delivering -- closing doesn't discard them, it just forbids any NEW sends. Second, and more subtly: the unbuffered recv() path normally registers as waiting when there's nothing to receive, on the assumption that a matching send() might still show up. Once the channel is closed, that assumption is provably false -- registering as waiting anyway would be a receiver waiting for something that can now never arrive, exactly the kind of bug that turns into a real, silent goroutine leak in Go.

Example

ch = Channel(maxsize=0)       # unbuffered
ch.send(1)                    # -> False (no receiver waiting -- would block)
ch.recv()                     # -> (False, None) (nothing to receive -- now waiting)
ch.send(42)                   # -> True (a receiver IS waiting now -- delivers)
ch.recv()                     # -> (True, 42)

ch.close()
ch.send(99)                   # raises ChannelClosedError
ch.recv()                     # -> (False, None) -- and does NOT register as waiting

Constraints

  • A buffered channel's send/recv never touches receivers_waiting at all -- that bookkeeping only matters for the unbuffered case.
  • Once a rendezvous delivers a value, receivers_waiting must go back down -- a second send() right after should NOT succeed unless another recv() registered as waiting again.
  • recv() must check the buffer BEFORE checking whether the channel is closed -- closing doesn't discard what's already buffered.

Real-world grounding

  • Go's channel implementation, including closechan's handling of pending receivers: src/runtime/chan.go

Real Go channels use actual goroutine blocking/waking (parking a goroutine on a wait queue, waking it when a match arrives, or when the channel closes) instead of a boolean return value -- and closing a real Go channel actually wakes every goroutine currently parked waiting to receive from it, handing each one the zero value. This exercise's "don't register as waiting on an already-closed channel" rule is the same guarantee, just checked at recv()-time instead of via an explicit wake-up.

hints
channel.pysign in to run
Loading...
test result

This is a preview. Sign in to run this exercise against the real tests.