The channels behavior of Golang

Source: Internet
Author: User
This is a creation in Article, where the information may have evolved or changed.

Brief introduction

When I first used Go channels work, I made a mistake to consider channels as a data structure. I see the channels as a queue that provides automatic synchronization access between Goroutines. This structural understanding leads me to write a lot of bad and complex concurrency code.

Over time, I realized that the best way is to forget that channels is a data structure and instead focus on its behavior. So now I'm talking about channels, and I'm only thinking about one thing: signaling (signal). A channel allows one goroutine to give another signal to a specific event. Signals are the core of everything that is done with the channel. The channel is seen as a signaling mechanism that allows you to write better code that clearly defines and precisely behaves.

To understand how a signal works, we must understand the following three features:

    • Delivery Guarantee
    • State
    • have data or no data

These three features together form the philosophy of design around the signal, and after discussing these features, I'll provide a series of code examples that demonstrate the signals that use these properties.

Delivery Guarantee

The delivery guarantee is based on the question: "Do I need to ensure that a signal sent by a particular goroutine has been received?" ”

In other words, we can give an example of Listing 1:

Listing 1

01 go func() {02     p := <-ch // Receive03 }()0405 ch <- "paper" // Send

Whether the sent goroutine need to guarantee the paper sent to the channel in line fifth will be received by Goroutine on the second line before proceeding.

Based on the answer to this question, you will know which of the two types of channels are used : unbuffered or buffered . Each channel provides a different behavior around delivery guarantees.

Figure 1

Guarantee is important, and if you don't think so, I have a lot of things to sell to you. Of course, I would like to make a joke, when your life is not protected when you are not afraid of it? When writing concurrent code, it is critical to have a strong understanding of whether a guarantee is needed. As you continue, you will learn how to make decisions.

State

The behavior of a channel is directly affected by its current state. The status of a channel isnil,open , or closed.

Listing 2 below shows how to declare or put a channel in these three states.

Listing 2

// ** nil channel// A channel is in a nil state when it is declared to its zero valuevar ch chan string// A channel can be placed in a nil state by explicitly setting it to nil.ch = nil// ** open channel// A channel is in a open state when it’s made using the built-in function make.ch := make(chan string)    // ** closed channel// A channel is in a closed state when it’s closed using the built-in function close.close(ch)

The status determines how the Send (send) and receive (receive) operation behaves.

Signals are sent and received through a channel. Do not say read and write because the channels does not perform I/O.

Figure 2

When a channel is nil , any attempt to send or receive the channel will be blocked. When a channel is in the open State, the signal can be sent and received. When a channel is placed in the closed state, the signal will not be sent, but the signal can still be received.

These states can provide different behaviors when you encounter different situations. When combined status and delivery assurance , as a result of your design choices, you can analyze the costs/benefits you incur. You can also quickly find errors simply by reading the code, because you know what the channel will show.

With data and no data

The final signal characteristics need to be considered if you need the signal to have data or no data.

A signal that has data in a channel is executed with a send.

Listing 3

01 ch <- "paper"

When your signal has data, it is usually because:

    • A goroutine is required to start a new task.
    • A goroutine conveys a result.

Countless data signals are passed by closing a channel.

Listing 4

01 close(ch)

When the signal has no data, it is usually because:

    • A goroutine was told to stop what it was doing.
    • A goroutine reports that they have been completed with no results.
    • A goroutine report that it has finished processing and shuts down.

There are exceptions to these rules, but these are the main use cases, and we will focus on these issues in this article. I think the exception to these rules is the initial code flavor.

One advantage of countless signals is that a single goroutine can give a lot of goroutines signals right away. A signal with data is usually one-to-one exchange of data between Goroutines.

There's a data signal.

When you use a data signal, depending on the type you need to guarantee, there are three channel configuration options to choose from.

Figure 3: A data signal

These three channel options are:unbuffered, Buffered >1 or Buffered =1.

    • Have a guarantee

      • A non-buffered channel gives you assurances that the signal being sent has been received.

        • Because the signal reception occurs before the signal is sent to completion.
    • No warranties

      • A size > 1 buffered channel does not guarantee that the transmitted signal has been received.

        • Because the signal transmission occurs before the signal transfer is complete.
    • Delay Guarantee

      • A size = 1 buffer channel provides a delay guarantee. It ensures that the previously sent signal has been received.

        • Because the first receive signal occurs before the second completed send signal.

The buffer size cannot be a random number, it must be calculated for some well-defined constraints. There is no infinity in the calculation, whether it is space or time, all things must have good definition constraints.

Countless data signals

Countless data signals are mainly used for cancellation, which allows one goroutine to send a signal to another to cancel what they are doing. Cancellation can be implemented with buffered and unbuffered channels, but it is better to use buffer channel without data sending.

Figure 4: No data signal

The built-in functions close are used for data-free signals. As explained in the Status section above, you can still receive signals when the channel is closed. In fact, any receive on a closed channel is not blocked, and the receive operation will always return.

In most cases, you want to use a standard library context package to implement countless data signals. The context package uses an unbuffered channel pass signal and the built-in function to close send no data signals.

If you choose to use your own channel instead of the context package to cancel, your channel should be chan struct{} type, which is a zero-space idiom used to indicate that a signal is only used for signal transmission.

Scene

With these features, the best way to further understand how they work in practice is to run a series of code scenarios. When I read and write the channel base code, I like to think of Goroutines as an adult. This image is very helpful to me and I will use it as an auxiliary tool below.

Data signal-Guaranteed-unbuffered Channels

When you need to know that a sent signal has been received, there are two things to consider. They are waiting for tasks and waiting for results .

Scenario 1-Waiting for a task

Consider the need to hire a new employee as a manager. In this scenario, you want your new employees to perform a task, but they need to wait until you're ready. This is because you need to hand them a report before they start.

Listing 5

Online Demo Address

01 func waitForTask() {02     ch := make(chan string)0304     go func() {05         p := <-ch0607         // Employee performs work here.0809         // Employee is done and free to go.10     }()1112     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)1314     ch <- "paper"15 }

In line 2nd of Listing 5, an unbuffered channel with attributes is created and the string data is sent along with the signal. On line 4th, an employee is hired and told to wait for your signal "on line 5th" before starting work. Line 5th is a channel receive that causes employees to block until you send a report. Once the report is received by the employee, the employee will perform the work and be able to leave at the time of completion.

You as a manager are concurrently working with your employees. So in line 4th, after you hire an employee, you find out what you need to do to unlock and signal the employee (line 12th). It is worth noting that it is not known how long it will take to prepare the report (paper).

Eventually you are ready to signal the employee, in line 14th, you execute a data signal, and the data is the report. As a non-buffered channel is used, you get a guarantee that once you're done, the employee has received the report. The receive occurs before the send.

Technically, all you know is that when your channel is sent, the employee receives the report. After two channel operations, the scheduler can choose to execute any statement it wants to execute. The next line of code that is executed by you or the employee is not certain. This means that using the PRINT statement will deceive you about the order in which events are executed.

Scenario 2-Waiting for results

In the next scenario, things are the opposite. At this point you want your employees to perform their tasks as soon as they are hired. Then you need to wait for the results of their work. You need to wait because you need them to send a report before you continue.

Listing 6
Online Demo Address

01 func waitForResult() {02     ch := make(chan string)0304     go func() {05         time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)0607         ch <- "paper"0809         // Employee is done and free to go.10     }()1112     p := <-ch13 }

Cost/Benefit

The Unbuffered channel provides a guarantee that the signal will be received when it is sent, which is fine, but nothing is at all cost. This cost is guaranteed to be an unknown delay. In the waiting task scenario, the employee does not know how long it will take you to send your report. In the waiting results scenario, you don't know how long it will take for the employee to send you the report.

In the above two scenarios, the unknown delay is what we have to deal with because it needs to be guaranteed. Without this guarantee, logic will not work.

With data signal-no guarantee-buffer Channels > 1

Scenario 1-fanout (fan out)

Fan-out mode allows you to throw a clearly defined number of employees at the same time to work on the issue. Since you have an employee on every task, you know exactly how many reports you will receive. You may need to make sure that your box has the right amount of space to receive all the reports. This is the benefit of your employees and there is no need to wait for you to submit their reports. But they do need to take turns to put the report in your box if they arrive at the box almost at the same time.

Once again assume you are the manager, but this time you hire a team of employees, you have a separate task and you want each employee to execute it. As each individual employee completes their task, they need to provide you with a report to put into the box on your desk.

Listing 7
Demo Address

01 func fanOut() {02     emps := 2003     ch := make(chan string, emps)0405     for e := 0; e < emps; e++ {06         go func() {07             time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)08             ch <- "paper"09         }()10     }1112     for emps > 0 {13         p := <-ch14         fmt.Println(p)15         emps--16     }17 }

In line 3rd of listing 7, a buffered channel with attributes is created and the string data is sent along with the signal. At this point, emps a 20 buffered channel will be created because of the variable declared in line 2nd.

Between lines 5th and 10th, 20 employees were hired, and they immediately began to work. In line 7th you don't know how long each employee will take. At this point in line 8th, employees send their reports, but this time the send does not block waiting to be received. Because there is room for each employee in the box, the transmission on the channel only competes with the employee who wants to send their report at the same time.

The code between 12 lines and 16 lines is all your operation. Here you wait for 20 employees to complete their work and send a report. In line 12, you are in a loop, and in 13 lines you are blocked in a channel waiting to receive your report. Once the report is received to complete, the report is printed in 14, and the local counter variable is consumed to indicate that an employee's opinion has completed his work.

Scene 2-drop

The drop mode allows you to lose your job when your employees are at full load. This facilitates the continuation of the client's work, and never puts pressure on or is an acceptable delay in the work. The key here is to know when you are full load, so you do not assume or over commit the amount of work you will try to complete. Often integration tests or metrics can help you determine this number.

Suppose you're a manager and you hire a single employee to get the job done. You have a separate task that you want employees to perform. When an employee finishes their task, you don't care to know they're done. The most important thing is that you can or can't put a new job into the box. If you cannot perform the send, then you know your box is full and the employee is full load. At this point, the new job needs to be discarded to keep things going.

Listing 8
Demo Address

01 func selectDrop() {02     const cap = 503     ch := make(chan string, cap)0405     go func() {06         for p := range ch {07             fmt.Println("employee : received :", p)08         }09     }()1011     const work = 2012     for w := 0; w < work; w++ {13         select {14             case ch <- "paper":15                 fmt.Println("manager : send ack")16             default:17                 fmt.Println("manager : drop")18         }19     }2021     close(ch)22 }

In line 3rd of listing 8, a buffered channel with attributes is created and the string data is sent along with the signal. Because of the constant declared in line 2nd cap , a channel with 5 buffers is created.

From line 5th to line 9th, a single employee is hired to handle the work, and one for range is used to cycle through the receive of the channel. Each time a report is received, it is processed on line 7th.

Between lines 11th and 19, you try to send a 20-minute report to your employees. At this point a select statement is used at the first of the 14th row case to perform the send. Because default the clause is used in the 16th line of the select statement. If the send is blocked, it is discarded because there is no extra space in the buffer to send by executing line 17th.

Finally, in line 21st, the built-in function close is called to close the channel. This will send a signal without data to the employee to indicate that they have completed and that once they have completed the assignment to their job can leave immediately.

Cost/Benefit

A buffered channel buffer greater than 1 provides an unsecured signal that is sent to be received. It is good to leave a guarantee that communication between the two goroutine can be reduced or there is no delay. In the fan out scenario, there is a buffer space for storing the reports that the employee will be sent. In the Drop scenario, the buffer is a measurement capability, and if the capacity is full, the work is discarded so that the work continues.

Of the two choices, this lack of assurance is what we have to face, because the delay is very important. The requirement of 0 to minimum latency does not cause problems for the overall logic of the system.

Data signal-Delay guaranteed-Buffer 1 channel

Scenario 1-Waiting for a task

Listing 9
Demo Address

01 func waitForTasks() {02     ch := make(chan string, 1)0304     go func() {05         for p := range ch {06             fmt.Println("employee : working :", p)07         }08     }()0910     const work = 1011     for w := 0; w < work; w++ {12         ch <- "paper"13     }1415     close(ch)16 }

In line 2nd of listing 9, a buffer-sized channel with a property is created and the string data is sent along with the signal. Between lines 4th and 8th, an employee is hired to handle the job. for rangeis used to cycle the reception of channel. In line 6th each time a report is received it is processed.

Between line 10th and line 13, you start sending your task to the employee. If your employees can run as fast as you send, the delay between you will be reduced. But each time you send a successful execution, you need to make sure that the last job you submit is being carried out.

In the last 15th line, the built-in function close is called to close the channel, which will send no data signals to the employee informing them that the work is done and can leave. However, the last job you submit will be for range received before the interruption.

Countless data signals-Context

In this final scenario, you will see Context how values from the package Context are used to cancel a running goroutine. All this work is done by changing a closed unbuffered channel to perform a no-data signal.

The last time you were a manager, you hired a single employee to get the job done, and this time you won't be waiting for the employee to finish his job in an unknown time. You are assigned a deadline, and if your employees do not finish the work on time, you will not be waiting.

Listing 10
Demo Address

01 func withTimeout() {02     duration := 50 * time.Millisecond0304     ctx, cancel := context.WithTimeout(context.Background(), duration)05     defer cancel()0607     ch := make(chan string, 1)0809     go func() {10         time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)11         ch <- "paper"12     }()1314     select {15     case p := <-ch:16         fmt.Println("work complete", p)1718     case <-ctx.Done():19         fmt.Println("moving on")20     }21 }

In line 2nd of listing 10, a time value is declared, which represents how long it will take the employee to complete their work. This value is used on line 4th to create a 50-millisecond timeout context.Context value. context the function of the package WithTimeout returns a Context value and a cancel function.

contextThe package creates a goroutine that, once the time value expires, closes the unbuffered Context channels associated with the value. No matter how it happens, you need to be responsible for calling the cancel function. This will clean up Context what has been created. cancelIt is possible to be called more than once.

In line 5th, once the function is interrupted, the cancel function is deferred executed. In line 7th, 1 buffered channels are created and it is used by employees to send the results of their work to you. On lines No. 09 and 12, employees are immediately put into work by mercenaries, and you do not need to specify how long it takes for the employee to complete their work.

Between lines 14th and 20, you use select a statement to receive at two channels. On the 15th line of the receive, you wait for the employee to send their results. On the 18th line of the receive, you wait context to see if the packet is sending a signal for 50 milliseconds to the time. Whichever signal you receive first, one will be processed.

An important aspect of this algorithm is the use of a buffered channels. If the employee is not finished on time, you will leave without giving the employee any notice. For the employee, on line 11th he will always send his report, you are not there to receive, he is blind. If you use a unbuffered channels, if you leave, the employee will always be blocked in that attempt to send the report to you. This can cause goroutine leaks. So a buffered channels is used to prevent this problem from happening.

Summarize

When using channels (or concurrency), it is important to ensure that the signal properties are in the channel state and during transmission. They will help you achieve the better behavior you need for concurrent programs and the algorithms you write. They will help you find bugs and sniff out potentially bad code.

In this article, I've shared some program examples to show that the signal properties work in different scenarios. There are exceptions to everything, but these patterns are a very good start.

As a summary review of these points, when and how to effectively think and use channels:

Language mechanism

    • Use channels to orchestrate and collaborate Goroutines:

      • Focus on signal properties rather than data sharing
      • With data signal and no data signal
      • Ask them for the purpose of synchronizing access to shared data

        • In some cases, the channel can be simpler for this problem, but the initial problem is.
    • unbuffered Channels:

      • Receive occurs before the Send
      • Yield: 100% guaranteed signal received
      • Cost: Unknown delay, do not know when the signal will be received.
    • With buffer channels:

      • The send occurs before the receive.
      • Gain: Reduces the blocking delay between signals.
      • Cost: no guarantee of when the signal will be received.

        • The larger the buffer, the less guaranteed.
        • A buffer of 1 can give you a delayed send guarantee.
    • Closed channels:

      • The shutdown occurs before receiving (like Buffering).
      • Countless data signals.
      • Perfect signal cancellation or cutoff.
    • Nil Channels:

      • Both send and receive are blocked.
      • Turn off the signal.
      • Perfect speed limit or short downtime.

Design philosophy

    • If any given send on the channel can cause a send Goroutine block:

      • Does not allow buffer channels greater than 1.

        • buffer greater than 1 must have cause/measure.
      • must know what happened when sending Goroutine blocked.
    • If any given send on the channel does not cause a send blocking:

      • Each send must have an exact buffer number.

        • fanout mode.
      • Has the maximum capacity for buffering measurements.

        • Drop mode.
    • Less is more for buffering.

      • When buffering is considered, do not consider performance.
      • Buffering can help reduce the blocking delay between signals.

        • Reducing blocking latency to 0 does not necessarily mean better throughput.
        • If a buffer can give you enough throughput, keep it.
        • buffering issues larger than 1 require a measurement size.
        • to find the minimum buffer to provide sufficient throughput
Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.