Practice of communication model based on channel

Keywords: Go Database

preface

As the data structure of Go core and the communication mode between Goroutine, Channel is an important structure supporting the high-performance concurrent programming model of Go language. This section will introduce the design principle, data structure and common operations of pipeline Channel, such as Channel creation, sending, receiving and closing.

Before entering the topic content, readers need to master the results of Read, Write and close operations of channel s in different states in the following table.

Figure from Cao Da

The most common and frequently mentioned design pattern in Go language is: do not communicate through shared memory, but share memory through communication. Although we can also use shared memory and mutex for communication in Go language, Go language provides a different concurrency model, namely communication sequential processes (CSP). Goroutine and channel correspond to entities in CSP and information transmission media respectively. Goroutine will transmit data through channel. This paper will introduce a variety of communication models based on channel implementation.

MPSC: multi producer single consumer model
In the MPSC application scenario, multiple producers are responsible for production data, and only one consumer is responsible for consumption data. This model can be implemented in two ways:

  1. Multiple producers share a channel to communicate with consumers
  2. Use your own unique channel to communicate with consumers

Variant 1: producer shared channel

As shown in the figure, multiple production goroutines on the left write data to the public channel, and only one consumption goroutine on the right reads data from this channel for processing.

Basic Edition
First, we define the delivered message structure:

`type Msg struct {

in int

}`

Then the producer is implemented as follows, in which the parameter sendchannel is the channel through which the producer and the consumer communicate

// producer
func producer(sendChan chan Msg) {
    for i := 0; i < 10; i++ {
        sendChan <- Msg{in: i}
    }
}

Consumers and message processing functions are defined as follows. The parameter sendchannel is the channel through which producers and consumers communicate. At present, the current message processing function process only prints out the message content.

// consumer
func consumer(sendChan chan Msg) {
    for v := range sendChan {
        process(v)
    }
}

// Message processing function
func process(msg Msg){
    fmt.Println(msg)
}

The model code of mpsc is as follows. First, create a channel for communication, and then start three producer goroutines and one consumer goroutine.

func mpsc() {

    sendChan := make(chan Msg, 10)

    for p := 0; p < 3; p++ {
        go producer(sendChan)
    }

    go consumer(sendChan)
}

The main function is as follows. select {} is to keep the goroutine where the main function is located blocked all the time. Otherwise, after the main function exits immediately, the producer and consumer goroutine may exit without executing or only partially executing.

`func main() {

mpsc()
select{}

}`

Complete code Online demonstration https://www.online-ide.com/IB... give the result as follows

{0}
{1}
{2}
{3}
{4}
{5}
{6}
{7}
{8}
{9}
{0}
{1}
{2}
{3}
{4}
{5}
{6}
{7}
{8}
{9}
{0}
{1}
{2}
{3}
{4}
{5}
{6}
{7}
{8}
{9}
fatal error: 
all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
    /home/kingeasternsun/1ba7ba19-5e66-4f4c-beb3-cb5e3e6d881e/main.go:9 +0x25
goroutine 9 [chan receive]:
main.consumer(0xc000072000)
    /home/kingeasternsun/1ba7ba19-5e66-4f4c-beb3-cb5e3e6d881e/main.go:25 +0xa9
created by main.mpsc
    /home/kingeasternsun/1ba7ba19-5e66-4f4c-beb3-cb5e3e6d881e/main.go:43 +0x9b
exit status 2


** Process exited - Return Code: 1 **

You can see that the printed numbers are crossed, indicating that multiple producers have performed writing concurrently. However, there are two errors in the program after the producer sends them:

  1. The first goroutine 1 [select (no cases)]: it means that select {} is always blocked
  2. Article 2 goroutine 9 [channel receive]: it means that after the producer sends the database, no new data is written to the channel, while the consumer consumes the database in the channel, and then reads the data from the empty channel will be blocked. Therefore, the above report fatal error: all goroutines are asleep - deadlock! This error

Because we are demo, in order to keep main alive and use select{}, mspc is often invoked in a continuous running program, so there is no such problem. ​

Of course, we can also directly fix this error, so that producers can send a message to consumers to quit after sending it.

Fix deadlock problem
We need to use sync.WaitGroup to synchronize before sending messages to consumers. How do producers send messages to consumers? At the same time, they should ensure that consumers exit after processing the previous messages. There are two options:

  1. Send a specially marked Msg, marking this message as a termination message
  2. close channel

The first scheme will cause additional memory consumption of communication. The second scheme is recommended. ​

First, modify the producer code as follows: a wg *sync.WaitGroup is added to the input, and the producer calls the wg.Done() after sending the data.

// producer
func producer(sendChan chan Msg, wg *sync.WaitGroup) {
    for i := 0; i < 10; i++ {
        sendChan <- Msg{in: i}
    }

    wg.Done()
}

Then the mpsc model is rewritten as follows:

func mpsc() {

    // Number of producers
    pNum := 3
    sendChan := make(chan Msg, 10)

    wg := sync.WaitGroup{}
    wg.Add(pNum)
    for p := 0; p < pNum; p++ {
        go producer(sendChan, &wg)
    }

    // Wait until the producers are finished and close sendChan to notify the consumers
    go func() {
        wg.Wait()
        close(sendChan)
    }()

    consumer(sendChan)
}

You can see the following changes in mpsc

  1. A new goroutine is created. Use wg.Wait() to wait for the producers to complete, and then close the channel;
  2. The consumer(sendChan) will no longer create a new goroutine to execute, so the mspc will become blocked and wait for the normal end of the consumer.

Because mpsc itself is blocked, we only need to call mpsc in main

`func main() {

mpsc()

}`

Complete code

Two way communication between producers and consumers
In the current model, the producer does not know the result of message processing after sending the message to the consumer. If the producer wants to know the result of message processing, how to change it? One of the more common methods is that each producer maintains its own private channel, and then sends its own private channel together with the message to the consumer when sending the message. After the consumer processes the message, the processing result is sent back to the producer through the channel in the message. ​

First, add a channel member in the message type definition to store the private channel of the producer

type Msg struct {
    in int
    ch chan int
}

At the beginning, the producer will create a unique channel, and then put this channel into Msg when sending messages. At the same time, the producer will start a new goroutine to accept the effect returned by consumers from this unique channel.

// producer
func producer(sendChan chan Msg, wg *sync.WaitGroup) {
    recvCh := make(chan int)
    go func() {
        for v := range recvCh {
            fmt.Println("recv ", v)
        }
    }()

    for i := 0; i < 10; i++ {
        sendChan <- Msg{in: i, ch: recvCh}
    }

    wg.Done()
}

Finally, modify the message processing function as follows, double the value in the accepted message and pass it back through the channel in the message.

// Message processing function
func process(msg Msg) {
    msg.ch <- 2 * msg.in
}

Complete code


So far, producers use a common channel to send messages to consumers. In another scheme, producers use their own unique channel to send messages to consumers. ​

Variant 2: producers use a unique channel to communicate with consumers

In this scheme, each producer maintains a unique channel to communicate with consumers, and consumers listen to these channels to obtain messages for processing.

Basic Edition

For the producer, a unique channel will be created and returned to the consumer for reading. At the same time, a goroutine will be created internally to send data to and from this channel. The code is as follows:

func producer(in []int) chan Msg {
    ch := make(chan Msg)
    go func() {
        for _, v := range in {
            ch <- Msg{in: v}
        }
        close(ch)
    }()
    return ch
}

The consumer will listen to the channel read messages of multiple producers at the same time. The corresponding consumers are as follows:

func consumer(ch1, ch2 chan Msg) {
    for {
        select {
        case v1 := <-ch1:
            fmt.Println(v1)
        case v2 := <-ch2:
            fmt.Println(v2)
        }
    }
}

The corresponding mpsc model is as follows

func mpsc() {
    ch1 := producer([]int{1, 2, 3})
    ch2 := producer([]int{4, 5, 6})

    consumer(ch1, ch2)
}

Complete code

Problems will be found during actual execution. After all producers send data to close their channels, consumers continue to receive data from the channels, but the received data values are all 0. The reason is that the closed channels are still readable, and two values will actually be returned when reading. The first value is a zero value and the second value is a bool value, Identify whether the current channel is closed, so we need to read the bool value in the code to determine whether the current channel is closed. In addition, one of the multiple channels in the select may be closed, but the other channels are not closed, and the data is still readable. How can consumers skip the closed channels? We can set the closed channels to nil. All channels reading and writing nil are blocked, so these channels will be skipped in select. ​

Repair version

According to the above, we need to make the following modifications

  1. Read the close flag of the channel to judge whether the current channel is closed
  2. If the current channel is close d, set the current channel to nil
  3. When all channel s are closed, the consumer exits

The code is as follows:

func consumer(ch1, ch2 chan Msg) {
    var v1 Msg
    var v2 Msg
    ok1 := true
    ok2 := true

    for ok1 || ok2 {
        select {
        case v1, ok1 = <-ch1:
            fmt.Println(v1)
            if !ok1 { //The channel is closed
                ch1 = nil
            }
        case v2, ok2 = <-ch2:
            fmt.Println(v2)
            if !ok2 { //The channel is closed
                ch2 = nil
            }
        }
    }
}

Complete code

SPMC: single producer multi consumer model

As shown in the figure, a single producer and multiple consumers communicate through a common channel. Producers write messages to the channel. Multiple consumers scramble to read messages from the channel for processing. This model is very similar to the FanOut model in a message queue. ​

The producer is responsible for writing messages to the channel and closing the channel after writing

func producer(ch chan Msg) {
    in := []int{1, 2, 3, 4, 5, 6}
    for _, v := range in {
        ch <- Msg{in: v}
    }
    close(ch)

}

The consumer is responsible for reading messages from the channel

func consumer(ch chan Msg) {
    for v := range ch {
        fmt.Println(v)
    }
}

The spmc model code is as follows:

func spmc() {

    ch := make(chan Msg)
    go producer(ch)
    go consumer(ch)
    go consumer(ch)
    go consumer(ch)

}

The spmc model is particularly convenient for closing, because there is only one producer, so the producer can close the channel after sending messages, and then downstream consumers can automatically exit after the data in the channel is read. When for range operates the channel, if the bool value returned by the channel is read to be false, it will exit the loop. ​

Because both producers and consumers in spmc are asynchronous, the logic can be executed normally only if main keeps blocking all the time.

`func main() {

spmc()
select {}

}`

Complete code

Another common way to write is as follows: producer creates a channel internally and then returns it

func producer() chan Msg {
    in := []int{1, 2, 3, 4, 5, 6}
    ch := make(chan Msg)
    go func() {
        for _, v := range in {
            ch <- Msg{in: v}
        }
        close(ch)
    }()
    return ch
}

The consumer then reads the message from the channel returned by the producer. The spmc model is as follows

func spmc() {

    ch := producer()
    go consumer(ch)
    go consumer(ch)
    go consumer(ch)

}

Complete code

Posted by spasme on Sun, 28 Nov 2021 23:52:47 -0800