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

In concurrent programming, many programming languages ​​adopt a shared memory/state model. However, Go differentiates itself from many by implementing  the Communicating Sequential Process (CSP) model. In CSP, a program consists of parallel processes that do not share state; instead, they communicate and synchronize operations through channels. Therefore, it becomes crucial for developers interested in adopting Go to understand how channels work. In this article, I will use pleasant metaphors to describe the Gophers operating their imaginary café, because I firmly believe that humans are better suited to learning visually.

Scenes

Partier, Candier and Stringer are running a cafe. Since making coffee takes more time than taking orders, Partier will be responsible for taking orders from customers and then passing those orders to the kitchen, where Candier and Stringer will prepare the coffee.

011465bd7bd2d53ab38812c98ace599b.png
 

unbuffered channel

Initially, the cafe operated in the simplest way: when a new order came in, the Partier placed the order in the lane and waited until either Candier or Stringer picked it up before proceeding to accept any new orders. 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 are still idle, waiting for new orders to arrive.

7221ab876b327e9d858f1387f3200fd2.png
 

unbuffered channel

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

715040732a52945761ff310d468b4ed1.png
 

Since both Stringer and Candier can accept new orders, new orders will be accepted by one of them immediately. However, specific recipients cannot be guaranteed or predicted. The choice between Stringer and Candier is undefined and depends on factors such as scheduling and the internals of the Go runtime. Suppose Candier gets the first order.

a9f9fa268d0cc10377aec091013be776.png
 

After Candier finishes processing the first order, she returns to the waiting state. If no new orders arrive, both Candier and Stringer workers will remain idle until Partier puts orders into the channel for processing again.

d82446bb2d56589d2bd2b8ca402609bf.png
 

When a new order arrives and both Stringer and Candier can process it. Even if Candier has just processed the previous order, the specific worker receiving the new order is still undefined. In this case, assume that Candier is assigned to the second order again.

ed49c996a3e42661c036caa009451114.png
 

A new order arrives  order3, which Candier is processing at the moment  order2, she is not  order := <-ch waiting on this line, and Stringer becomes the only available worker to receive it  order3. Therefore, he will receive it.

1fda92f75f4155599e1d68536a3a8dd2.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 the channel is unbuffered,  order4 putting into the channel blocks the Partier until either the Stringer or the Candier becomes available to receive  order4. This case requires special attention because I often see people complaining about unbuffered channels (using  make(chan order) or  make(chan order, 0) creating)

and a channel with a buffer size of 1 (  make(chan order, 1) created using) creates confusion. Therefore, they incorrectly expect  ch <- order4 immediate completion, allowing the Partier to accept before being blocked  order5. If you think so too, I've created a code snippet on the Go Playground to help you correct this misconception: https://go.dev/play/p/shRNiDDJYB4.

44cd09c341c94d75a1a14a96b572c7a2.png
 

Buffered channel

Unbuffered channels can work, but it limits overall throughput. It would be better if they only take a few orders and process them in the backend (kitchen) in order. This can be achieved with buffered channels . Now, even if the Stringer and Candier are busy processing their orders, as long as the channel is full, you can have the Partier put new orders into the channel and continue to accept additional orders, such as up to 3 unprocessed orders.

742e9ec62cf1d12d32ce2aa78c0e5684.png
 

By introducing buffered lanes, the cafe has increased its ability to handle more orders. However, the appropriate buffer size needs to be chosen carefully to keep customer 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 them and not be able to complete them in a timely manner. Additionally, caution is required when using buffered channels in ephemeral containerized (Docker) applications, as random restarts are expected and recovering messages from the channel may be challenging or even nearly 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 are similar to some extent. If you are familiar with blocking queues, understanding channels will definitely be easy.

Common uses

Channels are a fundamental and widely used feature of Go applications, serving a variety of purposes. Some common uses of channels include:

Goroutine communication : Channels allow messages to be exchanged 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 work pools, where multiple identical workers process incoming tasks from a shared channel. Fan-out, fan-in : Channels facilitate fan-out, fan-in patterns, where multiple Goroutines (fan-out) perform work and send results to a single channel, and another Goroutine (fan-in) consumes these results. Timeouts and deadlines : In conjunction with  select statements, channels can be used to handle timeouts and deadlines, ensuring that programs handle delays gracefully and avoid infinite waits.

I'll cover the different uses of channels in more detail in other articles. But now let us end this introductory blog by implementing the above mentioned cafe scene, observing the interaction between Partier, Candier and Stringer and how smooth communication and coordination can be achieved between them through channels, thus Enable efficient order processing and synchronization in cafes.

code example

package main


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


func main() {
    ch := make(chan order, 3)
    wg := &sync.WaitGroup{} // 更多关于 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, fine-tune it on your IDE and run it to better understand how channels work.

Guess you like

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