In-depth analysis of the operation results in each state of Go Channel

2023.06.20

In-depth analysis of the operation results in each state of Go Channel


Channels in golang are used to communicate between coroutines.  We deduced the results of operations in each state of the channel from the source code level.

Hello everyone, I am a fisherman.

Channel is a unique feature in golang, and it is also often asked in interviews.  I believe everyone has seen the following picture, what will be the result of the operation of the channel in different states.

picture

This picture sums it up pretty well.  But we cannot memorize these results by rote.  Understanding the underlying fundamentals enables understanding how these results come about.

Let's talk about it in three parts.  The first is the basic use of the channel, which shows the characteristics of the channel.  Then lead to the underlying data structure of the channel.  The underlying data structures are built around these features.  Finally, let' the s see how go implements these features based on the underlying data structure.

Basic use of channel

Channel definition and initialization

Define channels through var

Define a channel variable ch through var, which can receive integer data.  Of course, any other data type can also be specified.

var ch chan int
  • 1.
  • ch stands for variable name
  • chan fixed value.  Represents ch as a channel type
  • int means that the integer data is stored in the channel ch.
  • The default value of the ch variable is nil. For the nil channel, there will be special scenarios when operating, and we will explain it later.

Initialize the channel via make

Unbuffered channels and buffered channels can be initialized by make. The difference lies in whether the size of the buffer is specified in make. as follows:

var ch = make(chan int) //初始化无缓冲通道

var ch = make(chan int, 10) //缓冲区通道,缓冲区可以存10个元素
  • 1.
  • 2.
  • 3.

The difference between an unbuffered channel and a buffered channel can be reflected in two aspects: attributes and behavior:

  • The difference in terms of attributes: whether the channel has a buffer to temporarily store elements.
  • The difference in behavior: whether the sender and receiver are synchronous or asynchronous.
  • The difference from the underlying data structure: whether there is a buffer to temporarily store data. This will be explained in detail later.

channel operation

There are three operations for channels in golang: sending elements to the channel, receiving elements from the channel, and closing the channel. As follows: send elements to the channel:

var ch chan int = make(chan int, 10)

2 ->ch //发送元素

var item int
item <-ch //接收元素

close(ch) //关闭元素
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

in conclusion:

  • Channels have three operations: send, receive, and close.
  • There are three types of channels: nil channels, unbuffered channels, and buffered channels.
  • Channels have 2 states: closed state and not closed state.
  • The unclosed state of the buffer channel can be divided into the buffer full state and the buffer not full state.

So, what kind of data structure is the channel based on to complete these behaviors?

channel data structure

We first give the underlying data structure of the channel, as follows:

type hchan struct {
 qcount   uint           // total data in the queue
 dataqsiz uint           // size of the circular queue
 buf      unsafe.Pointer // points to an array of dataqsiz elements
 elemsize uint16
 closed   uint32
 elemtype *_type // element type
 sendx    uint   // send index
 recvx    uint   // receive index
 recvq    waitq  // list of recv waiters
 sendq    waitq  // list of send waiters

 // lock protects all fields in hchan, as well as several
 // fields in sudogs blocked on this channel.
 //
 // Do not change another G's status while holding this lock
 // (in particular, do not ready a G), as this can deadlock
 // with stack shrinking.
 lock mutex
}

type waitq struct {
 first *sudog
 last  *sudog
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • twenty one.
  • twenty two.
  • twenty three.
  • twenty four.
  • 25.

According to the above structure definition, explain the meaning of each field in turn:

  • buf: points to an array, which represents a queue, and realizes a circular queue by combining sendx and recvx fields. Cache the corresponding element. The buffer channel is implemented using this field.
  • qcount: How many elements are currently in the buf queue.
  • dataqsiz: represents the capacity of the queue buf. When using make to initialize, the specified number of elements exists in this field.
  • elemsize: The byte size of an element. According to the size of the element, the size of the capacity of buf can be initialized. You can know how many bytes of space should be allocated to buf through elemsize*capacity.
  • closed: Indicates whether the channel is closed. Its value is only 0 and 1. 1 means that the channel is closed. 0 means not closed.
  • elemtype: represents the type of element.
  • sendx: represents the location where the next element should be sent
  • recvx: represents the position of the next received element.
  • recvq: represents the coroutine queue waiting to receive elements
  • sendq: represents the coroutine queue for sending elements.

According to the above results, it is easy to understand when drawing a graph, as follows:

picture

The difference between buffered and unbuffered channels

By definition, both buffered and unbuffered channels are initialized by make. The difference lies in whether the capacity of the channel is specified on the make function. as follows:

unbufferCh := make(chan int) //初始化非缓冲区通道

bufferCh := make(chan int, 10) //初始化一个能缓冲10个元素的通道
  • 1.
  • 2.
  • 3.

From the perspective of the underlying data structure of the channel, the non-buffered channel will not initialize the buf field in the structure. The buffer channel will initialize the buf field. This field points to a memory area. As shown below:

picture

Channel sending and receiving process

Through the source code, we have sorted out the flow chart of sending data to the channel and receiving data from the channel. This flow chart includes both the sending and receiving processes of the buffered channel and the unbuffered channel, so it looks more complicated. But that's okay, we'll break down the graph below.

picture

Through the above process, one thing you need to pay attention to is that whether it is in the sending or receiving operation, the corresponding thread is first obtained from the waiting queue. If there is, it will receive or send directly; if the waiting queue has no coroutine, then Check to see if there is a buffer. This point requires everyone to pay extra attention.

picture

Operation of each state channel

unbuffered channel

According to the above-mentioned unbuffered channel, in essence, there is no buffer. Just do not specify the capacity of make during initialization. In fact this is also called synchronous send and receive. For a channel in this state, when sending data, if there is a waiting receiving coroutine in the receiving queue, the sending can be successful; otherwise, it enters the blocking state. vice versa. The flow chart is the red arrow part in the figure, as follows:

picture

Simplify it again:

  • When sending data to no buffer, if there is a coroutine waiting to receive, the sending is successful; otherwise, the sending coroutine enters a blocked state.
  • When receiving data from no buffer, if there is a coroutine waiting to be sent, the reception is successful; otherwise, the receiving coroutine enters a blocked state.

Then, the above diagram can be simplified as follows:

picture

In addition, you need to pay extra attention to the send and receive operations of non-buffer channels. If it is sent and received in the main function, it will cause a deadlock. as follows:

func main() {
 var ch = make(chan int)
 <-ch
 fmt.Println("the End")
}

//或
func main() {
 var ch = make(chan int)
 ch <- 2
 fmt.Println("the End")
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

picture

Therefore, for the send and receive operations of non-buffer channels, the main problem is that it may cause blocking. Unless, both sending and receiving coroutines exist and must be in different coroutines.

buffered channel

A channel with a buffer means that there is a buffer in the channel, and both sending and receiving can operate on the buffer. Also known as asynchronous send and receive. In the state of the buffered channel, for the sending operation, the state of the buffered channel is divided into two states: the buffer is full and the buffer is not full. According to the sending flow chart above, when the buffer is full, it will naturally be unable to send any more, and it will enter the waiting queue for sending. Block at the same time, waiting to be woken up by the receiving coroutine.

For receiving operations, the state of a buffered channel is divided into two states: buffer empty and not full. Similarly, if there is no data to receive when the buffer is empty, it will naturally enter the receive waiting queue. At the same time, it enters the block and waits to be woken up by the sending coroutine.

picture

Channels that are closed

Closing the channel is done through the **close** function. Essentially closing a channel is to set the closed field in the channel structure to 1. From the source code it is known that:

  • Close the nil channel: panic
  • Close the closed channel: panic. This can be understood in this way, it does not make any sense to close an already closed channel.

picture

Send a message to a closed channel

Sending a message to a closed channel will cause a panic. This is easy to understand, because the channel has been closed, just to prevent sending messages. The following code:

picture

Receive a message from a closed channel

The operation succeeds when receiving a message from a closed channel. But it will be different according to whether there are elements in the channel:

  • If there are no more elements in the channel, a false status will be returned.
  • If there are elements in the channel, it will continue to receive the elements in the channel until the reception is complete, and return false.

picture

You see, the code is actually very simple. Let's disassemble the code, which is the flowchart on the right.

nil channel

The default value of channel-type variables defined in the following ways is nil.

var ch chan int
  • 1.

A nil channel is equivalent to an underlying structure with no channel allocated

The following are the various operations intercepted from the source code and the corresponding operation results. From the source code you can get:

  • Closing the nil channel will panic
  • Receiving and sending messages from the nil channel will block

picture

Summarize

Channels in golang are used to communicate between coroutines. We deduced the results of operations in each state of the channel from the source code level. Finally, to summarize: the buffer channel:

  • As long as there is buffer space, it can be sent successfully. Blocking occurs unless the buffer space is full.
  • As long as there are elements in the buffer space, the reception can be successful.  Blocks unless there are no elements.

nil channel:

  • A nil channel is one that has no underlying data structures initialized.  Both sending and receiving block because there is no room to store any elements.  Closing the nil channel will trigger a panic.

Closed channel:

  • Sending a message to a closed channel will cause a panic.
  • Receiving a message from a closed channel will succeed.
  • Closing a closed channel will also cause a panic.