Using Channels In C# .NET – Part 1 – Getting Started

This post is part of a series on Channel in C# .NET. Of course, it’s always better to start at Part 1, but you can skip anywhere you’d like using the links below.

Part 1 – Getting Started
Part 2 – Advanced Channels
Part 3 – Understanding Back Pressure


I’ve recently been playing around with the new Channel<T> type that was introduced in .NET Core 3.X. I think I played around with it when it was first released (along with pipelines), but the documentation was very very sparse and I couldn’t understand how they were different from any other queue.

After playing around with them, I can finally see the appeal and the real power they posses. Most notable with large asynchronous background operations that need almost two way communication to synchronize what they are doing. That sentence is a bit of a mouthful, but hopefully by the end of this series it will be clear when you should use Channel<T>, and when you should use something more basic like Queue<T>.

What Are Channels?

At it’s heart, a Channel is essentially a new collection type in .NET that acts very much like the existing Queue<T> type (And it’s siblings like ConcurrentQueue), but with additional benefits. The problem I found when really trying to research the subject is that many existing external queuing technologies (IBM MQ, Rabbit MQ etc) have a concept of a “channel” and they range from describing it as a completely abstract thought process vs being an actual physical type in their system.

Now maybe I’m completely off base here, but if you think about a Channel in .NET as simply being a Queue with additional logic around it to allow it to wait on new messages, tell the producer to hold up because the queue is getting large and the consumer can’t keep up, and great threadsafe support, I think it’s hard to go wrong.

Now I mentioned a bit of a keyword there, Producer/Consumer. You might have heard of this before and it’s sibling Pub/Sub. They are not interchangeable.

Pub/Sub describes that act of someone publishing a message, and one or many “subscribers” listening into that message and acting on it. There is no distributing of load because as you add subscribers, they essentially get a copy of the same messages as everyone else.

In diagram form, Pub/Sub looks a bit like this :

Producer/Consumer describes the act of a producer publishing a message, and there being one or more consumers who can act on that message, but each message is only read once. It is not duplicated out to each subscriber.

And of course in diagram form :

Another way to think about Producer/Consumer is to think about you going to a supermarket checkout. As customers try to checkout and the queue gets longer, you can simply open more checkouts to process those customers. This little thought process is actually important because what happens if you can’t open any more checkouts? Should the queue just keep getting longer and longer? What about if a checkout operator is sitting there but there are no customers? Should they just pack it in for the day and go home or should they be told to just sit and wait until there is customers.

This is often called the Producer-Consumer problem and one that Channels aims to fix.

Basic Channel Example

Everything to do with Channels lives inside the System.Threading.Channels. In later versions this seems to be bundled with your standard .NET Core project, but if not, a nuget package lives here : https://www.nuget.org/packages/System.Threading.Channels.

A extremely simple example for channels would look like so :

static async Task Main(string[] args)
{
    var myChannel = Channel.CreateUnbounded();

    for(int i=0; i < 10; i++)
    {
        await myChannel.Writer.WriteAsync(i);
    }

    while(true)
    {
        var item = await myChannel.Reader.ReadAsync();
        Console.WriteLine(item);
    }
}

There’s not a whole heap to talk about here. We create an “Unbounded” channel (Which means it can hold infinite items, but more on that further in the series). And we write 10 items and read 10 items, at this point it’s not a lot different from any other queue we’ve seen in .NET.

Channels Are Threadsafe

That’s right, Channels are threadsafe. Meaning that multiple threads can be reading/writing to the same channel without issue. If we take a peek at the Channels source code here, we can see that it’s threadsafe because it uses a combination of locks and an internal “queue” to synchronise readers/writers to read/write one after the other.

In fact, the intended use case of Channels is multi threaded scenarios. For example, if we take our basic code from above, there is actually a bit of overhead in maintaining our threadsafe-ness when we actually don’t need it. So we are probably better off just using a Queue<T> in that instance. But what about this code?

static async Task Main(string[] args)
{
    var myChannel = Channel.CreateUnbounded();

    _ = Task.Factory.StartNew(async () =>
    {
        for (int i = 0; i < 10; i++)
        {
            await myChannel.Writer.WriteAsync(i);
            await Task.Delay(1000);
        }
    });

    while(true)
    {
        var item = await myChannel.Reader.ReadAsync();
        Console.WriteLine(item);
    }
}

Here we have a separate thread pumping messages in, while our main thread reads the messages out. The interesting thing you’ll notice is that we’ve added a delay between messages. So how come we can call ReadAsync() and things just…. work? There is no TryDequeue or Dequeue and it runs null if there are no messages in the queue right?

Well the answer is that a Channel Reader’s “ReadAsync()” method will actually *wait* for a message (but not *block*). So you don’t need to do some ridiculously tight loop while you wait for messages, and you don’t need to block a thread entirely while waiting. We’ll talk about this more in upcoming posts, but just know you can use ReadAsync to basically await a new message coming through instead of writing some custom tightly wound code to do the same.

What’s Next?

Now that you’ve got the basics down, let’s look at some more advanced scenarios using Channels.

Learned Something New?

By buying me a coffee (As a one off or monthly), you ensure I have the optimal caffeine levels to continue reading patch notes and wading through shoddy documentation to give you nothing but the important bits of .NET.
ENJOY THIS POST?
Join over 3,000 subscribers who are receiving our weekly post digest, a roundup of this weeks blog posts.
We hate spam. Your email address will not be sold or shared with anyone else.

7 comments

    1. Did you like the rest of the article? It can be good to step out of the engineering mindset where we point out everything that could be improved. How to create a new task is very off the mark for this article’s subject.

      1. Note from me. I thought it was fine 🙂 Can always learn something new and I actually didn’t know about these differences between Task.Run and Task.Factory.StartNew. Hence why I approved the comment. Maybe someone else reading through will learn something new too 🙂

  1. Useful, but I haven’t managed to apply it yet. I need to go somewhere else to understand the significance of the “async” and “await” keywords in this code, and you haven’t pointed me to anywhere I can learn that.

  2. Any idea if this works on high traffic scenarios? How about comparing it to RabbitMQ I can see that is a lighter version of it but does it handles the messages the same way? Like in RabbitMQ once the message goes into the queue soon or later will be consumed no matter if the server is down for a few minutes?

Leave a Reply

Your email address will not be published. Required fields are marked *