This article has participated in the Denver Nuggets Creators Camp 3 “More Productive writing” track, see details: Digg project | creators Camp 3 ongoing, “write” personal impact.

In a previous article, we introduced the use of channels, portals. A classic sentence is:

In Go, it is advocated to communicate with shared memory rather than with shared memory. In fact, it is advocated to transmit data by sending and receiving messages through channels.

Let’s take a closer look at Chan.

Chan data structure

SRC /runtime/chan.go defines the channel data structure as follows:

type hchan struct {
	qcount   uint  // Total number of elements in the queue
	dataqsiz uint  // The size of the ring queue can hold the number of elements
	buf      unsafe.Pointer // Ring queue pointer
	elemsize uint16  // The size of each element
	closed   uint32  // Indicates the shutdown status
	elemtype *_type // Element type
	sendx    uint   // Send the index to the position of the element in the queue when it is written

	recvx    uint   // To receive the index, the element is read from that position in the queue
	recvq    waitq  // The goroutine queue waiting to read messages
	sendq    waitq  // The goroutine queue waiting to write messages
	lock mutex  // mutex, chan does not allow concurrent reads and writes
}
Copy the code

The circular queue

Chan internally implements a circular queue as a buffer, the length of which is specified when the chan is created.

As shown below, we create a channel schematic that can cache 6 elements:

  • Dataqsiz indicates that the queue length is 6, and 6 elements can be cached;
  • Buf points to the memory of the queue, which has two elements left;
  • Qcount means there are two more elements in the queue;
  • Sendx indicates the location of the data store to be written later. The value range is[0, 6);
  • Recvx indicates that data is read from this location. The value range is[0, 6);

Waiting queue

  • Read data from a channel. If the channel’s buffer is empty, or there is no buffer, the current goroutine is blocked.
  • Write data to a channel. If the channel’s buffer is full or there is no buffer, the current goroutine will block.
  • A blocked goroutine hangs in a channel’s wait queue.
    • Because the block caused by the read is woken up by the Goroutine writing to the channel.
    • The block caused by a write is woken up by a Goroutine reading data from a channel.

Recvq has several goroutine blocks waiting to read data.

Note that in general, at least one of recvq and sendq is empty. The only exception is when the same Goroutine uses a SELECT statement to

Channel writes data and reads data at the same time.

The type information

A channel can only pass a value of one type, and the type information is stored in the HCHAN data structure:

  • Elemsize: Type size, used to locate elements in buF.
  • Elemtype: specifies the type used to assign values during data transfer.

The lock

As we know, channels are concurrency-safe, meaning that only one Goroutine can read or write a channel at a time.

The channel to read and write

Create a channel

Creating a channel initializes the hchan structure, whose type information and buffer length are passed in by the make statement. The size of the BUF is determined by the element size and buffer length.

Create channel pseudocode:

func makechan(t *chantype, size int) *hchan{
  var c *hchan
  c = new(hchan) c.bouf = malloc(element type size x size) C.elemSize = Element type size C.elemType = Element type C.Dataqsiz = sizereturn c
}
Copy the code

Write data to channel

The process is as follows:

  1. If recVQ is not empty, G will be directly fetched from RECVQ and written to the buffer. Finally, G will wake up to end the sending process.
  2. If there is a free space in the buffer, the data is written to the buffer to end the sending process.
  3. If there is no free space in the buffer, send data is written to G, add the current G to sendq, and go to sleep waiting to be woken up by reading goroutine.

Read data from channel

The process is as follows:

  1. If the waiting sending queue sendq is not empty and there is no buffer, G is directly taken out from Sendq, data is read out from G, and finally G is woken up to end the reading process.
  2. If sendQ is not empty, the buffer is full. Read data from the head of the buffer, write data from G to the tail of the buffer, wake up G and end the reading process.
  3. If there is data in the buffer, the data is fetched from the buffer, ending the reading process.
  4. Add the current goroutine to recVQ, go to sleep, and wait for the goroutine to wake up.

Shut down the channel

Closing a channel wakes up all G in RECVQ, and the data position that should have been written to G is nil. Wake up all G’s in sendq, but they panic.

Panic also appears in the following scenarios:

  1. Close a channel with a value of nil
  2. Close closed channels
  3. Write data to a closed channel

usage

Single channel

A channel that can only be sent or received is a one-way channel.

Unidirectional channel declaration

Just add the operator to the base declaration:

send := make(ch<- int) // Can only send data to channels
receive := make(<-ch int) // Can only receive data from channels
Copy the code

Example:

package main

import (
	"fmt"
)
// Only channels can be sent
func send(s chan<- string){
	s <- "Tiny Guest Nest"
}
// Can only receive channels
func receive(r <-chan string){
	str := <-r
	fmt.Println("str:",str)
}
func main(a) {
	// Create a two-way channel
	ch := make(chan string)
	go send(ch)
	receive(ch)
}

// Result: STR: micro guest nest
Copy the code

select

Select enables multiplexing, that is, listening for multiple channels at the same time.

  • If you find which channel has data generated, execute the corresponding case branch
  • If more than one case branch can be executed at the same time, one is randomly selected
  • If none of the case branches can be executed, the select waits

Example:

package main

import (
	"fmt"
)

func main(a) {
	ch := make(chan int.1)
	for i := 0; i < 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
			fmt.Println("--", i)
		}
	}
}
Copy the code

Running results:

-- 0
0
-- 2
2
-- 4
4
-- 6
6
-- 8
8
Copy the code

A select case statement reading a channel does not block, even though there is no data in the channel. This is because when the case statement is compiled and calls the read channel, it explicitly passes in a non-blocking parameter. If no data is read, the current Goroutine is not queued, but returned directly.

range

The range is used to continuously read data from a channel, similar to traversal, blocking the current Goroutine when there is no data in the channel, just like blocking when reading a channel.

Example:

for ch := range chanName {
  fmt.Printf("chan: %d\n", ch)
}
Copy the code

Note: If the goroutine writing to this channel exits, the system will panic if it detects this, otherwise range will block permanently.