The Crystal Programming Language Forum

Single-consumer channels

I’ve been experimenting with something along the lines of Rust’s MPSC (multi-producer/single-consumer) channels. The result is here. With this shard you can still call send and receive just like with plain Channels, and if you want to do a non-blocking receive you call receive?.

Part of this experiment was based on the frustration some of us felt when things like Channel#empty? were removed from the API and I finally decided to create this shard instead of using code like channel.@queue.empty? any time I didn’t want to block when there were no items in the channel. :slightly_smiling_face:

To be clear, I think removing methods like Channel#empty? was a good decision because they gave a false sense of security when you were consuming the same channel on multiple fibers. Most of my own use cases for channels in Crystal are consuming from a single fiber, though, and it seems a few other members of the Crystal community do the same. And one of the nice things about consuming from a single fiber is that you don’t have to worry about race conditions that the stdlib Channel insulates us from.

2 Likes

What is the advantage over doing this?

ch = Channel(Nil).new

select
when obj = ch.receive?
  # do something with obj
else
  puts "nothing to receive and we're not blocking"
end

Other than less verbosity.
That’s how I dealt with the removal of Channel#empty?.

The performance of select is 300x slower:

require "benchmark"
require "mpsc"

empty_channel = Channel(String).new(10)
empty_mpsc = MPSC::Channel(String).new
value = nil # Assigning to this variable on each iteration so LLVM doesn't optimize out the benchmark blocks
iterations = ENV.fetch("ITERATIONS", "1000").to_i
Benchmark.ips do |x|
  x.report "MPSC::Channel" { iterations.times { value = empty_mpsc.receive? } }
  x.report "Channel" do
    iterations.times do
      select
      when value = empty_channel.receive?
      else
        value = nil
      end
    end
  end
end
pp value # Use the assigned value so LLVM doesn't optimize the benchmark blocks out

# MPSC::Channel   2.09M (478.88ns) (± 0.75%)   0.0B/op         fastest
#       Channel   6.94k (144.20µs) (± 0.74%)  281kB/op  301.11× slower

This performance difference can be important in a tight loop. In the NATS client maintained by Synadia, trying to use select resulted in a reduction in publish performance of 6.2M messages/sec.

7 Likes

Wow that’s insane. Good thing you went through the trouble then!