Go concurrency explained visually - Channels

In concurrent programming, many programming languages ​​adopt a shared memory/state model. However, Go differentiates itself from many languages ​​by implementing Communicating Sequential Processes (CSP). In CSP, a program consists of parallel processes that do not share state but use channels to communicate and synchronize their operations. Therefore, it becomes critical for developers interested in adopting Go to understand how channels work. In this article, I'm going to illustrate channels using the cute metaphor of Gophers running their fictional cafe, because I'm a firm believer that humans learn more visually.

scene

Partier, Candier and Stringer run a cafe. Since making coffee takes more time than taking orders, Partier will assist in taking customer orders and then passing those orders to the kitchen, where Candier and Stringer make the coffee.

407a7ee78158179ec43e576ab86bfa5d.png
 

Gopher's Cafe

unbuffered channel

Initially, the cafe operated in the simplest way: whenever a new order came in, the Partier put the order in the channel and waited for either Candier or Stringer to pick it up before accepting the new order. This communication between the Partier and the kitchen is achieved through an unbuffered channel, using  ch := make(chan Order) Create. When there are no pending orders in the channel, even if both Stringer and Candier are ready to accept new orders, they will remain idle, waiting for new orders to arrive.

a7a89150e73f79a5f2a009a8aaf39f60.png
 

unbuffered channel

When a new order is received, the Partier puts it into the channel so that the order can be accepted by one of the Candier or Stringer. However, the Partier must wait for one of them to get an order from the channel before continuing to accept new orders.

e07aa24dc42c12858855596399702a50.png
 

Since both Stringer and Candier can accept new orders, the order will be accepted by one of them immediately. However, there is no guarantee or prediction as to which specific recipient will obtain an order. The choice between Stringer and Candier is non-deterministic and depends on factors such as scheduling and the internals of the Go runtime. Suppose Candier obtains the first order.

2934935331472e841430139ae0e66763.png
 

After Candier finishes processing her first order, she goes back to waiting. If no new orders arrive, both workers, Candier and Stringer, remain idle until Partier puts another order into the channel for them to process.

c00dd14bc53dfd243aa9f50f62734c6f.png
 

When a new order arrives and both Stringer and Candier can process it, even though Candier has just processed the previous order, the specific worker who receives the new order is still undefined. In this case, assume that Candier is again assigned as the recipient of the second order.

4ebd334a682903ef86f189679957060b.png
 

order3 Candier was processing  the new order  when it arrived order2, and she was not waiting in  order := <-ch line, so Stringer became the only  order3 staff member available to receive it. Therefore, he will receive it.

74c7465da2d4a72141ca1eabc9876f5b.png
 

order3 Arrives shortly after being  sent to Stringer order4 . At this point, Stringer and Candier are already busy processing their respective orders, and no one is available to take them  order4. Because channels are not buffered, putting  order4 into the channel blocks the Partier until the Stringer or Candier can receive it  order4 .  This case deserves special attention because I often see people confused between unbuffered channels (using  make(chan order) or  make(chan order, 0) creating) and channels with a buffer size of 1 (using  creating). make(chan order, 1)Therefore, they mistakenly expect  ch <- order4 immediate completion, allowing the Partier  ch <- order5 to accept before being blocked  order5. If you think so, I've created a code snippet on the Go Playground to help you correct your misunderstanding https://go.dev/play/p/shRNiDDJYB4.

2c4426f6a9faafc4aaa93b93427e0c7c.png
 

Buffered channel

Unbuffered channels are efficient, but they limit overall throughput. If they only pick up

It would be better to take some orders so that they can be processed sequentially in the backend (kitchen). This can be achieved by using buffered channels . Now, even if Stringer and Candier are busy processing their orders, Partier can still put new orders into the channel and continue to accept other orders as long as the channel is full, for example with up to 3 pending orders.

4fc6afe236daf3e0f1f953a43aacaa28.png
1*TCrrdUaa7XPcmRSDIO7xEQ.png

With the introduction of buffered lanes, the cafe has increased its ability to handle more orders. However, it is important to choose an appropriate buffer size to keep client wait times reasonable. After all, no customer wants to endure long wait times. Sometimes it may be more acceptable to reject new orders than to accept new orders but not be able to complete them in a timely manner. Additionally, be careful when using transient containerized (Docker) applications with buffered channels, as random restarts are expected, in which case recovering messages from the channel may be a challenging or even impossible task.

Channel vs blocking queue

Although fundamentally different, blocking queues in Java are used for communication between threads, while channels in Go are used for communication between Goroutines, blocking queues and channels show similarities to some extent. If you are familiar with blocking queues, it will definitely be easier to understand channels.

Common uses

Channels are a basic and widely used feature in Go applications and can be used for a variety of purposes. Some common use cases for channels include:

Goroutine communication : Channels allow message exchange between different Goroutines, enabling them to collaborate without directly sharing state. Work Pools : As shown in the example above, channels are often used to manage worker pools, where multiple identical workers process incoming tasks from a shared channel. Distribution and Aggregation : Channels facilitate the distribution and aggregation pattern, where multiple Goroutines (distribution) perform work and send results to a single channel, while another Goroutine (aggregation) consumes these results. Timeouts and deadlines : Channels  select , used in conjunction with statements, can be used to handle timeouts and deadlines, ensuring that the program can handle delays gracefully and avoid infinite waits.

I will explore the different uses of channels in more detail in other articles. But, for now, let's end this introductory blog by implementing the above cafe scenario and observe how channels play a role in it. We'll explore the interaction between Partier, Candier, and Stringer and observe how channels facilitate smooth communication and coordination between them, enabling efficient order processing and synchronization in a cafe.

Demo code

package main


import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"
)


func main() {
    ch := make(chan order, 3)
    wg := &sync.WaitGroup{}
    wg.Add(2)


    go func() {
        defer wg.Done()
        worker("Candier", ch)
    }()


    go func() {
        defer wg.Done()
        worker("Stringer", ch)
    }()


    for i := 0; i < 10; i++ {
        waitForOrders()
        o := order(i)
        log.Printf("Partier: I %v, I will pass it to the channel\n", o)
        ch <- o
    }


    log.Println("No more orders, closing the channel to signify workers to stop")
    close(ch)


    log.Println("Wait for workers to gracefully stop")
    wg.Wait()


    log.Println("All done")
}


func waitForOrders() {
    processingTime := time.Duration(rand.Intn(2)) * time.Second
    time.Sleep(processingTime)
}


func worker(name string, ch <-chan order) {
    for o := range ch {
        log.Printf("%s: I got %v, I will process it\n", name, o)
        processOrder(o)
        log.Printf("%s: I completed %v, I'm ready to take a new order\n", name, o)
    }


    log.Printf("%s: I'm done\n", name)
}


func processOrder(_ order) {
    processingTime := time.Duration(2+rand.Intn(2)) * time.Second
    time.Sleep(processingTime)
}


type order int


func (o order) String() string {
    return fmt.Sprintf("order-%02d", o)
}

You can copy this code, tweak it and run it on your IDE to better understand how channels work.

Related series of articles:

Go language channels using the Communicating Sequential Process (CSP) model

Go concurrency visual explanation-Select statement

Guess you like

Origin blog.csdn.net/weixin_37604985/article/details/132769764