skip to content

In a channel-based pipeline, how should closing a channel work: who is allowed to close it, what do receivers observe afterwards, and what breaks when several producers share one channel?

level: seniorimportance: should knowfreq 38%

answer

  1. close = broadcast end-of-stream, not release
  2. drain buffered values, then closed forever
  3. sender closes; receiver never closes
  4. N producers -> barrier + one coordinator closes
  5. abandoned consumer = blocked sender = leak

basics

~20 s

Closing broadcasts end-of-stream. Receivers drain any buffered values first, then every receive reports closed immediately. Only the sending side may close, and with several producers you need a coordinator that closes once all of them have finished, never each producer closing.

solid answer

~60 s

Close is a one-way, broadcast signal meaning no more values will ever be sent. It is not a resource-release call and not a cancellation of the producers. Receivers see the buffered values first, in order, and only then observe the closed state — typically as a flag alongside a zero value, or as loop termination. That is what makes close usable as end-of-stream: a consumer loops until closed and knows it has seen everything. Sending on a closed channel is an error, and so is closing twice, so ownership must be unambiguous: the sending side closes, the receiving side never does. With N producers on one channel, no single producer knows it was last, so they must not close. Have them signal completion — a counter or wait group — and let a coordinating task close after all have finished. To stop producers early you need the opposite-direction signal: a cancel channel the producers select on. Closing the data channel would only make them crash on their next send. Finally, consumers that abandon a channel leave senders blocked forever, so shutdown means either draining or cancelling.

code

text · 16 lines
text
out = channel(capacity = 16)
pending = N

for each producer p:
  spawn:
    for item in p.items:
      select:
        case send(out, item): pass
        case receive(cancel): break        # give up cleanly
    signal(done)                            # never close(out) here

spawn:
  wait_for(done, times = N)
  close(out)                                # exactly one closer

for v in receive_until_closed(out): use(v)

go deeper

for a junior

Know that close means no more values, that buffered items are still delivered, and that the consumer loop ends when the channel closes.

for a middle

Add the ownership rule (sender closes, exactly once) and that sending on a closed channel is an error.

for a senior

Cover the multi-producer barrier, the separate upstream cancel channel, and the silent leak from abandoned consumers with blocked senders.

for a principal

Present shutdown as a protocol with an invariant per stage — on input closed or cancel, flush, close my output, return — and argue for in-band error values so termination and failure propagate through the same path.

## What close means Close marks a channel as terminated for sending. It is a broadcast: every current and future receiver observes it, without anyone having to send N sentinel values for N consumers. That is its main value over the alternative protocol of sending a poison-pill message, which requires knowing how many consumers exist. The semantics that matter: - Buffered values already in the channel are still delivered, in order, before closure is observed. Close does not discard data. - After the buffer is empty every receive returns immediately with a closed indication rather than blocking. Repeated receives keep reporting closed; it is a stable state, not a one-shot event. - Sending on a closed channel is a programming error, usually fatal. Closing an already-closed channel is likewise an error. - Close travels only in the send-to-receive direction. It tells consumers there is no more input; it tells producers nothing. ## Who closes Because sending after close is fatal, the right to close must sit with the side that sends, and there must be exactly one closer. Two consequences follow. A receiver must never close its input. It cannot know whether a producer is about to send, and closing turns that producer's next send into a crash. If a consumer wants to stop early, it signals upstream on a separate cancel channel. With multiple producers on a shared channel, no producer can close: none of them knows it is the last. The standard structure is a completion barrier — each producer signals when done, a coordinator waits for all N signals, and only then closes the channel. The coordinator is often a small task whose whole job is wait, then close. ## Draining and leaks The symmetric hazard is a consumer that stops reading. Any producer blocked in a send on that channel stays blocked forever; the task is never scheduled again and never collected, so memory and any resources it holds leak. Nothing reports this — the program keeps working, just with a permanently parked task and, over time, one per abandoned pipeline. So shutdown of a pipeline has two obligations, not one. Downstream must be told there is no more data (close). Upstream must be told to stop producing (cancel), and every blocking send upstream must be written as a select over the send and the cancel signal so it can abandon the send. Draining the channel to completion is the alternative when the remaining volume is bounded and cheap. ## Errors and early termination Close carries no payload, so it cannot express failed. Two common conventions: send a value type that can hold either an item or an error, so the consumer sees the failure in-band and then the close; or pair the data channel with an error channel that the consumer checks after the data channel closes. The in-band form composes better across pipeline stages because ordering is preserved. ## Shutdown as a protocol The pieces fit into one shape that generalizes to any channel topology. Cancellation flows upstream through a channel that is closed once by whoever owns the pipeline; closure of that channel is the broadcast stop, which is exactly why close-with-no-value is the idiomatic way to notify many listeners. Data and end-of-stream flow downstream through the data channels, closed by each stage when its input has closed and its own work is flushed. Every stage's rule is: on input closed, finish, close my output, return; on cancel, abandon, close my output, return. If each stage obeys that rule, termination propagates to the end of the pipeline with no task left blocked. ## Checklist for a review Exactly one closer per channel, on the sending side. Multiple producers means a completion barrier plus a coordinator. Every long-lived send and receive is inside a select with the cancel channel. Consumers that return early either drain or cancel. Nobody sends after close.

  • Why not have every producer just close the shared channel when it finishes?
    The first close ends the stream for consumers while other producers are still sending, so their next send hits a closed channel and fails, and the second close is itself an error. Closing must be a single event owned by one party that knows all sending has finished, which is the coordinator behind a completion barrier.
  • A consumer hits an error and returns without draining its input. What is the observable consequence?
    Any producer blocked in a send on that channel never resumes, so the task is parked forever along with whatever memory and handles it holds. Nothing crashes, so the leak is silent and accumulates one parked task per occurrence. Prevent it by having consumers signal cancellation on the way out and by writing producer sends as a select over send and cancel.

saying these in an interview costs you the question

  • Treating close as a cleanup or resource-release call rather than an end-of-stream signal.
  • Letting the receiving side close its input channel.
  • Believing close discards buffered values instead of delivering them first.
  • Assuming closing the data channel stops the producers — it only makes their next send fail.
  • Closing a channel from each of several producers.

context