What really are Channels in Kotlin?

Joao Foltran
CodeX
Published in
7 min readJul 27, 2022

--

While many other programming languages rely exclusively on the concept of threads, Kotlin takes advantage of Coroutines to provide an intuitive, less error-prone and efficient set of tools to perform asynchronous work. Unsurprisingly, Kotlin applications often use many coroutines that need to communicate with each other, and Channels provide an elegant solution for this. This article introduces you to Channels by diving into helpful examples. Let’s get started!

image by pixabay

Prerequisites

This article assumes you understand the concepts to create and use Coroutines. If you do not, I’d recommend to take a look at the docs beforehand.

Basic Concepts

Conceptually, Channels are communication pathways that allow you to pass a stream of values from one Coroutine to another. The elements are processed in the same order as they arrive in, which is one of the similarities between Channels and BlockingQueues. In a nutshell,

One coroutine, the producer, can send some data to a channel, while the other coroutine, the receiver, can receive this data from it. While a channel can have multiple receivers, each element sent through the channel is handled only once by the receivers.

image by author

In practice, channels are implemented using the generic Channel interface to allow it to transfer any type of data. Channel also implements the SendChannel and ReceiveChannel interfaces, which separate the exposed functionality to avoid errors. For example, only SendChannels can send elements and close the channel altogether and only ReceiveChannels can receive elements and check whether the channel is empty.

To use this encapsulation, channels are cast as SendChannel when used in producer objects and as ReceiveChannel when used in receiver objects.

Before going further, let’s make matters more concrete by taking a look at a simple example that sends Animals through a channel. In this and the other examples in this article, we declare variables explicitly for clarity, even when we don’t need to do so.

As expected, Animals.RABBIT is sent through the channel, gets received and its value is printed in the console.

Channel Capacity

Before diving into how Channels work under the hood, it’s crucial to understand the different types of Channel available. In a nutshell,

Channels differ in how many elements they can store internally until they are properly received. This buffer size affects how send operations are performed, since we cannot simply send elements to a Channel that’s already full.

Behind the scenes, there are 4 capacity options with which we can initialize a channel. We explore each of them in detail below.

Unlimited Channel

An unlimited channel can indefinitely buffer elements that were sent to it.

image by author

Building from the Animals example above, this means you can call send in as many Animals you’d like and the channel will store the elements until you decide to start calling receiver.receive(). In other words, the send operation never gets suspended.

Note that in the definition of the SendChannel interface, send is a suspend function, meaning that, in simple terms, it can be paused and resumed later. Although this is not the case in Unlimited channels, in the cases we will analyze below send may get suspended in case the channel cannot support sending more elements.

Even though unlimited channels have an unlimited buffer size, if no memory is available and you try to send more elements to it, you will eventually get an OutOfMemoryException.

Buffered Channel

A buffered channel can internally buffer at most the number of elements that is passed to it in the channel’s constructor.

image by author

Because of this buffer limit, if we send an element while the channel is full, this call is suspended until more space is freed up. The buffer can be freed up by calling receive for example.

If this example is still a bit abstract, don’t worry, as we will cover this suspension mechanism in more detail later in the article.

Rendezvous Channel

A Rendezvous channel is a Buffered channel with capacity 0.

image by author

This means that a send operation gets suspended until the corresponding receive operation is called. By default, channels are Rendezvous, meaning that to initialize a Rendezvous channel it suffices to call the constructor with no arguments:

Conflated Channel

A Conflated channel buffers exactly one element and in turn overwrites the previously sent elements if they have not yet been received.

image by author

For example, in the image above, if the channel sends the blue ball, and before it is received, it sends the yellow ball, the yellow ball overwrites the blue ball, meaning that the call to receive now fetches the yellow ball.

In this case, the send operation never gets suspended, since subsequent calls to it will simply overwrite unreceived values.

What about Receiving values?

We’ve discussed how different channels send values and in which cases the send operation is suspended, and now we will take a closer look at how channels receive elements.

Note that receive is also a suspend function, as declared in the ReceiveChannel interface, meaning that it can be suspended just like the send operation. In a nutshell, for any channel type:

If a channel is not empty, a call to receive retrieves an element, otherwise it suspends execution until an element in the channel is available.

To solidify this knowledge, let’s take a look at some examples.

Rabbits, Elephants and Sloths

We will build upon the Animals example in the beginning of this article to explore how each channel type behaves under the same condition.

The shared code is quite simple, we initialize a channel, launch a coroutine that sends Animals.RABBIT, then Animals.ELEPHANT. In another coroutine, we send Animals.SLOTH and in yet another coroutine, we receive all three Animals. The sections below explain the outputs for when the channel is each of the 4 possible channel types.

Rendezvous. In this case, the channel has no buffer, so any send operation will be suspended until the corresponding receive is called. And that’s exactly what we observe by following the diagram below.

image by author
  1. producerA from coroutine #1 sends RABBIT, but gets suspended since there are no corresponding receivers, yet;
  2. producerB from coroutine #2 sends SLOTH, but gets suspended for the same reason;
  3. Upon execution of coroutine #3, both suspended values are received and printed. And coroutines #1 and #2 are freed up, in this order. But there’s no third value to be received, yet, so coroutine #3 also gets suspended;
  4. producerA from coroutine #1 sends ELEPHANT but does not suspend since there’s already a corresponding receiver from coroutine #3 suspended. In fact, since the channel is no longer empty, coroutine #3 is now free as well;
  5. Coroutine #1 proceeds to finish execution, and since there are no more operations in coroutine #2, it also finishes. Finally, coroutine #3 receives ELEPHANT and finishes executing as well.

Note that “freed” here means that the coroutines no longer need to remain suspended and will be scheduled to be executed once the current coroutine and any other coroutines scheduled beforehand are done executing. Remember we are operating on a single thread, so only one coroutine can execute at a given moment.

The output prints the following:

RABBIT
SLOTH
coroutine 1 done
coroutine 2 done
ELEPHANT
coroutine 3 done
Process finished with exit code 0

Unlimited. In this case, the channel’s buffer size is not bound by any value, so send operation won’t ever be suspended. You can check out how the code executes in the diagram below.

image by author

The output prints the following:

coroutine 1 done
coroutine 2 done
RABBIT
ELEPHANT
SLOTH
coroutine 3 done
Process finished with exit code 0

Now a quick note. For this and the next examples, we will abstract the written explanation since the diagrams should be able to show all the steps with clarity. If you have any questions, don’t hesitate to leave a comment in the article!

Buffered (More specifically, a buffer with capacity 2). In this case, the send operations get suspended when when the buffer capacity is at 2 and we try to send an additional element. The diagram below depicts the remaining of the execution flow.

image by author

The output prints the following:

coroutine 1 done
RABBIT
ELEPHANT
SLOTH
coroutine 3 done
coroutine 2 done
Process finished with exit code 0

Conflated. This is an interesting case since the completion of the receiver coroutine actually never finishes. Due to the nature of conflated channels, the send operations overwrite each other, but when the actual receive operations are called, the channel only contains 1 element in it, so the second receive call gets suspended and the coroutine stays idle and does not finish executing, as shown in the diagram below.

image by author

The output prints the following (notice that exit code 0 is not shown as the process is still running):

coroutine 1 done
coroutine 2 done
SLOTH

Thanks for reading! Hope you enjoyed reading this article as much as I enjoyed writing it. If you like the content, don’t forget to leave a clap and follow for more!

--

--