What are Channels in Golang?

Learn via video courses
Topics Covered

Overview

In the Golang, a channel acts as a medium through which goroutine communicates with each other with a lock-free mode of communication. It is a technique in golang that allows one goroutine to exchange data or to communicate with another goroutine and hence it is known as a channel. In this article, we will cover all about channels in golang and how goroutine works in a channel.

What is Channel?

Channel is a way through which goroutines communicate with other goroutines for sending data. The channel by default is bi-directional which means the data can be sent or received through the same channel as shown in the diagram below:

What is Channel

How to Define a Channel?

Channels in golang can be defined as means through which various goroutines communicate with each other. You can think of them as a pipeline through which different concurrent goroutines can connect and communicate. The communication however is bi-directional by default which means you can send and receive using the same channel, the communication happens when both sides are ready.

Syntax

For making a channel, use the function make(chan [value-type]), where [value-type] is the data type of the value that is to be sent and received, for example, int. For sending and receiving values channel <- is used where <- is the channel operator. For closing a channel, close(channel) is used and after closing the channel, no value is been sent to the channel.

Implementation

Following are the steps for implementation of channels in Golang:

  • The first step is to make a channel with a type string and function start with greet as a goroutine.
  • The greet function is blocked when <- c is encountered and it waits to receive a value.
  • The main function of the goroutine is to send value to the channel that the greet function prints when received.

Output

Read and Write Operations on Channel

For performing read and write operations, let’s say the first goroutine which is G1 wants to write the data on the channel, following are the operations to be performed:

  • Acquiring the lock, if the channel is to be modified then, the lock is to be acquired. Hence, in this case, the G1 will acquire the lock even before the data is written in the channel.
  • Performing an enqueue operation- we know that the buffered channel is a circular queue that is used for holding the data. Before enqueueing the data the goroutine does copying of memory operation on the data and puts the copied data in the buffer slot.
  • Releasing the lock, once the enqueue operation is performed, the lock is released for performing further operations.

Internals of Channels

The Goroutine scheduler is one of the most important factors in go run-time. It is used for tracking goroutine and is scheduled for running in a pool of threads of each process. Goroutine and threads are two separate things but except for the fact that goroutine is dependent on threads for it to execute and the performance of the go program. The main idea of a goroutine is to run concurrently and be light-weighted, just like threads. In goroutine there are three types of main C-structs that help in keeping track of all the things to execute the runtime scheduler: The G struct- represents a single goroutine that contains the necessary fields to keep the track of its stack and its current status. It contains a reference to the code and is responsible for running it. This can be seen from the figure.

  • The M struct- is the runtime representation of go’s thread. It has a pointer to the fields like G’s global queue, the G that is running currently a handler to the scheduler, and its cache.
  • The sched struct- is a single struct that is global and it keeps track of various queues like M’s and G’s. This can be seen in fig.

Internals of channels Internals of channels 2

Types of Channels

There are two types of channels available in golang according to the behavior of data exchange:

  • Unbuffered channel- it is used for performing synchronous communication between a buffered channel as well as goroutines.
  • Buffered channel- it is used for performing asynchronous communication. Here is a code block that creates buffered and unbuffered channels:

Properties of Channel

In golang, a channel does a lot of things, it has some properties which are as given below:

  • The channels in golang are goroutine safe
  • The values can be stored and passed between goroutines through the channel
  • The semantics that the channel provides is FIFO
  • The channels cause goroutines to unblock and block

Channel Structure

A channel is a data structure that handles goroutines in a synchronized manner. The channel internally has a circular queue, lock, etc. Let’s take an example to understand this better. When we write c:= make(chan int, 10) creates a channel using the keyword hchan struct having the following fields:

To understand each parameter of the channel internally, look at the parameters given below:

  • qcount- it is used for holding the count of data/item in the queue
  • dataqsize- it is the size of a circular queue and is used in the case of a buffered channel whereas the second parameter is used for making a function.
  • elemsize- it is defined as the size of the channel with respect to a single element.
  • buf- it is also known as an actual circular queue where the data is used in the case of the buffered channel.
  • closed - it indicates whether the channel is closed or not. The syntax for closing the channel is close(<channel_name>). The default value for this channel is 0 and is set to 1 when the channel is closed.
  • sendx and recvx- it indicates the current index of a circular queue or buffer and when the data is added the sendx increases, whereas when the data is received, the recvx increases.

Write in Case of Buffer Overflow

We know that the go routine can add up to the capacity of the buffer. But when no more space is left with the buffer, the go scheduler comes into the picture. To understand this effectively, let's take an example. Let's say that the goroutine G1 wants to write data, the go scheduler will either block or pause the G1 and wait until it receives data from G2. when G2 consumes all data then the go scheduler makes G1 active and pauses G2. This is how it works in case of buffer overflow.

Go Runtime Scheduler

Go schedulers are used for handling goroutines and multiplexing the goroutines on the operating system thread. Various scheduling models like 1:1, N:1, M, etc. The go scheduler however uses M scheduling model. This means that there is a number of goroutines as well as threads, the scheduler schedules the M goroutines on N threads as shown below:

Thread 1 OS:

Go Runtime Scheduler

Thread 2 OS:

Go Runtime Scheduler 2

There are two OS threads, with the scheduler running six goroutines and swapping it as and when needed.

  • M- it represents the OS thread that is managed by OS entirely and is similar to that of the POSIX thread and M stands for machine.
  • G- it represents goroutine and is a resizeable stack that contains information about scheduling, etc.
  • P- it is a context for scheduling and is a single thread that is used for running the Go code and to multiplex M goroutines to N go threads. Here P stands for processor. Go Runtime Scheduler 3

Summary of a Send Operation for Buffered Channels

To summarise it:

  • Acquire the lock either on the hchan struct or on the entire channel
  • Check if there is any sudog or goroutine waiting for recvq, if yes then put the elements into the stack directly.
  • If the recvq is empty then check the buffer space. If there is a space then copy the memory of the data.
  • In case the buffer is full, then create a sudog under sendq

Unbuffered channel always works like a direct send. To summarise the send and receive operation of the unbuffered channel:

  • Sender - here sender will create a sudog since there is no receiver and the receiver will then receive the value from the sudog.
  • Receiver - it will create sudog in recvq in return, the sender will directly put the data into the receiver’s stack.

Examples

Illustration of the Dining Philosopher's Problem

Let's assume that thefive philosophers relent and all of them are sitting at a round table with a bowl of spaghetti. The forks are kept adjacent between each of the pair of philosophers. Here each of the philosopher must think and then eat alternately however for philosophers to eat spaghetti they must have both, the left and the right fork. Only one of the five philosopher can hold a fork at a time, thus if another philosopher is not using it, the fork cannot be utilized by that philosopher. An individual philosopher must set down both of the forks after finishing their meal in order for the forks to be made available to others. A philosopher may use the fork on their left or right as it becomes available, but they must use both forks before they can begin to eat. There is an endless supply of spaghetti and an infinite demand, thus there is no restriction on how much spaghetti may be consumed or how much room there is in the stomach.

The problem description is how to design a routine of behavior by using a concurrent algorithm which should work such that no philosopher will starve, this means each of them can continue to switch between eating and thinking, keeping in mind that no philosopher can know the information about others that when they want to eat or think.

Output:

Illustration of Checkpoint Synchronization

A problem with multiple task synchronization is checkpoint synchronization. Think of a workshop where numerous people are putting the finishing touches on a machine. They combine the information after they have each finished their respective tasks. Because there is no store, the worker who completed their section first must wait for the others to finish before beginning their next. The checkpoint at which tasks synchronize themselves before diverging is the process of putting the pieces together.

Output:

Illustration of Producer-Consumer Problem

The issue is that a producer and consumer processes share a single, non dynamic size buffer that serves as a queue. The producer's responsibility is to produce data, store it in the buffer, and then start over. The consumer is simultaneously consuming all the data or taking it out of the buffer, one piece at a time. The challenge is to ensure that neither the producer nor the consumer will attempt to delete data from a buffer that is already full. If the buffer is full, the producer should either go to sleep or trash data. The producer restarts filling the buffer after receiving a notification from the consumer the next time an item is removed from the buffer. The consumer has the same option to fall asleep if it discovers the buffer is empty. The consumer is awakened each time the producer inserts data into the buffer.

Output:

Illustration of Sleeping Barber Problem

The barber features a waiting area with many chairs and a cutting room with one barber's chair. When a client's hair is finished being cut, the barber dismisses them and checks to see if anyone else is waiting in the waiting area. If there are, he returns to the chair and trims the hair of one of them. If not, he goes back to the chair and falls asleep there. When a client arrives, they each turn to see what the barber is doing. The client awakens the barber if he is dozing off and takes a seat in the cutting room chair. The client waits in the waiting area while the barber cuts the hair. The client waits in the waiting area while the barber cuts the hair. The client sits at a vacant chair in the waiting area and waits for their turn. The customer moves on if there isn't a chair available.

Output:

Conclusion

  • In the Golang, a channel acts as a medium through which goroutine communicates with each other with a lock-free mode of communication. It is a technique in golang that allows one goroutine to send data or to communicate with another goroutine and hence it is known as a channel.
  • Channel is a way through which goroutines communicate with other goroutines for sending data. The channel by default is bi-directional which means the data can be sent or received through the same channel.
  • Syntax
    • For making a channel, use the function make(chan [value-type]), where [value-type] is the data type of the value that is to be sent and received, for example, int.
  • Goroutine scheduler is one of the most critical factors of go run-time. It is used for tracking goroutine and is scheduled for running in a pool of threads of each process. Goroutine and threads are two separate things but except for the fact that goroutine is dependent on threads for it to execute and the performance of the go program. The main idea of a goroutine is to run concurrently and be light-weighted, just like threads. In goroutine there are three types of main C-structs that help in keeping track of all the things to execute the runtime scheduler.