Closing Channels in Crystal

Our first example demonstrates how to close channels, which is useful for communicating completion to the channel’s receivers.

# In this example we'll use a `jobs` channel to
# communicate work to be done from the main fiber
# to a worker fiber. When we have no more jobs for
# the worker we'll close the `jobs` channel.
jobs = Channel(Int32).new(5)
done = Channel(Bool).new

# Here's the worker fiber. It repeatedly receives
# from `jobs`. In Crystal, we use `receive?` to check
# if the channel is closed. If it returns `nil`, it means
# the channel has been closed and all values have been received.
# We use this to notify on `done` when we've worked all our jobs.
spawn do
  loop do
    job = jobs.receive?
    if job
      puts "received job #{job}"
    else
      puts "received all jobs"
      done.send(true)
      break
    end
  end
end

# This sends 3 jobs to the worker over the `jobs`
# channel, then closes it.
(1..3).each do |j|
  jobs.send(j)
  puts "sent job #{j}"
end
jobs.close
puts "sent all jobs"

# We await the worker using the synchronization approach
# we saw earlier.
done.receive

# Reading from a closed channel succeeds immediately,
# returning nil. We can use `receive?` to check if
# the channel is closed.
value = jobs.receive?
puts "received more jobs: #{!value.nil?}"

To run the program, save it as closing_channels.cr and use the Crystal compiler:

$ crystal closing_channels.cr
sent job 1
received job 1
sent job 2
received job 2
sent job 3
received job 3
sent all jobs
received all jobs
received more jobs: false

In Crystal, channels are closed using the close method. When a channel is closed, any further attempts to send on it will raise an exception. Receivers can use the receive? method to check if a channel is closed.

The concept of closed channels leads naturally to our next example: iterating over channels.