Goroutine and its use examples [Go language Bible notes]

Goroutines

Concurrent programs refer to programs that perform multiple tasks at the same time. With the development of hardware, concurrent programs become more and more important. The Web server processes thousands of requests at a time. Tablet PC and mobile app will perform various computing tasks and network requests in the background while rendering user images. Even the traditional batch processing problems, such as reading data, computing and writing output, now use concurrency to hide the operation delay of I/O in order to make full use of multiple cores of modern computer equipment. The performance of computers is increasing at a nonlinear rate every year.

Concurrent programs in Go language can be implemented by two means. This chapter explains goroutine and channel, which support "communicating sequential processes" or CSP for short.

CSP is a modern concurrent programming model. In this programming model, the value will be passed in different goroutines, but it is still limited to a single instance in most cases.

(note to the author: CSP is one of the main contributions of C.A.R.Hoare, a Turing Award winner. It is a formal language for modeling concurrent processes. The proposal of this theory is a strict algebraic proof process for the correctness of a series of concurrent processes. CSP is a complete one semester course for master students in school, with high threshold. Interested and capable readers can search the original thesis Or leave an email in the comment area. As long as you know that CSP is a strictly mathematically proven concurrency model theory for security critical systems, you don't need to further understand it.)

Chapter 9 covers the more traditional concurrency model: multithreaded shared memory, which may be more familiar if you have written concurrent programs in other mainstream languages. Chapter 9 also introduces some risks and pitfalls caused by concurrent programs.

Although Go's support for concurrency is one of many powerful features, it is still difficult to track and debug concurrent programs. The intuition formed in linear programs often leads us astray. If this is the first time that readers are exposed to concurrency, it is recommended to spend a little more time thinking about the examples in these two chapters.

Goroutines

Goroutine, Chinese translation is generally a collaborative process, but it is just the same as the Chinese translation of conceptual Coroutines. You can understand that it is a basic unit of concurrency in Go language. In fact, it is better to translate it into Go process.

In Go language, each concurrent execution unit is called a goroutine. Imagine that a program here has two functions, one for calculation and the other for output. It is assumed that the two functions have no calling relationship with each other. A linear program calls one of these functions first and then the other. If the program contains multiple goroutines, calls to two functions may occur at the same time. An example of such a program will be seen below.

If you have used threads provided by the operating system or other languages, you can simply compare the goroutine class to a thread, so that you can write some correct programs. The essential difference between goroutine and thread will be discussed in section 9.8.

When a program starts, its main function runs in a separate goroutine, which we call main goroutine. The new goroutine will be created with the go statement. Grammatically, a go statement is an ordinary function or method call preceded by the keyword go. The go statement causes the functions in its statement to run in a newly created goroutine. The go statement itself completes quickly.

f()    // call f(); wait it to return
go f() // create a new goroutine that calls f(); don't wait

In the following example, main goroutine will calculate the value of the 45th element of the Fibonacci sequence. Since the calculation function uses inefficient recursion, it will run for a long time. During this period, we want the user to see a visible sign to indicate that the program is still running normally, so let's make an animated small icon:

func main() {
    go spinner(100 * time.Millisecond)
    const n = 45
    fibN := fib(n)  // slow
    fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

func spinner(dealy time.Duration) {
    for {
        for _, r := range `-\|/`{
            fmt.Printf("\r%c", r)
            time.Sleep(delay)
        }
    }
}

func fib(x int) int {
    if x < 2 {
        return x
    }
    return fib(x-2) + fib(x-1)
}

After the animation is displayed for a few seconds, the call of fib(45) returns successfully, and the result is printed:

Author's note: the animation is a loading animation with "-" / | "as four key frames, which is expressed as a horizontal line rotating 360 degrees. It is vivid and vivid. Readers can realize it by themselves.

Fibonacci(45) = 1134903170

Then the main function returns. When the main function returns, all goroutines will be directly interrupted and the program will exit. Generally speaking, there is no other programming method to let one goroutine interrupt the execution of another except to exit from the main function or directly terminate the program.

But then we can see a way to achieve this goal. Through the communication between goroutines, one goroutine requests other goroutines, and the requested goroutine ends the execution by itself.

Notice how the two independent units here are combined, spinning and Fibonacci calculations. In separate functions, but the two functions execute at the same time.

Example: concurrent Clock service

Network programming is a field of concurrency. Because the server is the most typical program that needs to process many connections at the same time, these connections generally come from clients that initiate requests independently of each other. In this section, we will explain the net package of go language. This package provides the basic components for writing a network client or server program, whether the communication between the two is using TCP, UDP or Unix domain sockets. The methods in the net/http package we used in the first chapter are also part of the net package.

Our first example is a sequential clock server that writes the current time to the client every second:

// Clock1 is a TCP server that periodically writes the time

package main

import (
    "io"
    "log"
    "net"
    "time"
)

func main() {
    listenner, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)  // e.g., connection aborted
            continue
        }
        handleConn(conn)  // handle one connection at a time
    }
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("15:04:05\n"))
        if err != nil {
            return  // e.g., client disconnected 
        }
        time.Sleep(1 * time.Second)
    }
}

The Listen function creates a net.Listener object that will Listen for incoming connections on a network port. In this example, we use the localhost:8000 port of TCP. The Accept method of the listener object will block directly until a new connection is created, and then a net.Conn object will be returned to represent the connection.

The handleConn function handles a complete client connection. In a for loop, use time.Now() to get the current time and write it to the client. Since net.Conn implements the io.Writer interface, we can write content directly to it. This loop is executed until the write fails. The most likely reason is that the client actively disconnects. In this case, the handleConn function will use the defer call to close the connection on the server side, then return to the main function and continue to wait for the next connection request.

The time.Time.Format method provides a way to format date and time information. Its parameter is a format template that identifies how to format the time. This format template is limited to Mon Jan 2 03:04:05PM 2006 UTC-0700. There are 8 parts (day of the week, month, day of the month, etc.). The previous template can be combined in any form; The part appearing in the template will be used as a reference to output the time format. In the above example, we only use hours, minutes and seconds. Many standard time formats are defined in the time package, such as time.RFC1123. The same strategy is also used in the reverse operation time.Parse of formatting.

In order to connect to the server in the example, we need a client program, such as netcat (nc command), which can be used to perform network connection operations.

$ go build gopl.io/ch8/clock1
$ ./clock1 &
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C

The client displays the time sent by the server. We use Control+C to interrupt the execution of the client. On Unix system, you will see a response like ^ C. If nc is not installed in your system, you can use telnet to achieve the same effect, or you can use our following simple telnet program written in go to simply create a TCP connection with net.Dial:

// Netcat1 is a read-only TCP client.
package main

import (
    "io"
    "log"
    "net"
    "os"
)

func main() {
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    mustCopy(os.Stdout, conn)
}

func mustCopy(dst io.Writer, src io.Reader) {  // Parameter type description conn returned by net.Dial is io.Reader type
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

This program will read the data from the connection and write the read content to the standard output until it meets the end of file condition or an error occurs. mustCopy is a function that we will use in several examples in this section. Let's run two clients at the same time for a test. Two terminal windows can be opened here. The output of one of them is on the left and the output of the other is on the right:

$ go build gopl.io/ch8/netcat1
$ ./netcat1
13:58:54                               $ ./netcat1
13:58:55
13:58:56
^C
                                       13:58:57
                                       13:58:58
                                       13:58:59
                                       ^C
$ killall clock1

The kill command is a Unix command-line tool that can kill all processes with matching names with a given process name as a parameter.

The second client must wait for the first client to complete the work, so that the server can continue to execute backward; Because the server program here can only process one client connection at a time. Here we make a small change to the server program to support Concurrency: add the go keyword to the place where handleConn function is called, so that each call of handleConn enters an independent goroutine.

for {
    conn, err := listener.Accept()
    if err != nil {
        log.Print(err)
        continue
    }
    go handleConn(conn)  // handle connections concurrently
}

Now multiple clients can receive time at the same time:

$ go build gopl.io/ch8/clock2
$ ./clock2 &
$ go build gopl.io/ch8/netcat1
$ ./netcat1
14:02:54                               $ ./netcat1
14:02:55                               14:02:55
14:02:56                               14:02:56
14:02:57                               ^C
14:02:58
14:02:59                               $ ./netcat1
14:03:00                               14:03:00
14:03:01                               14:03:01
^C                                     14:03:02
                                       ^C
$ killall clock2

Author's note: The difference between adding the keyword go and not adding the keyword go is:

  • No go
    • The handleConn(conn) function is executed in the main coroutine, waiting for the returned result before continuing the next cycle;
  • Add go
    • A new coroutine is created to execute the handleConn(conn) method, and the main coroutine directly continues the next cycle

Example: concurrent Echo service

Each connection of the clock server will play a goroutine. In this section, we will create an echo server, which will have multiple goroutines in each connection. Most echo services will only return what they read, as the following simple handleConn function does:

func handleConn(c net.Conn) {
    io.Copy(c, c)  // Note: ignoring errors
    c.Close()
}

A more interesting echo service should simulate the "echo" of an actual echo. At first, it should use the uppercase hello to represent "loud sound", and then return a moderated Hello after a short delay. Then, an all lowercase Hello indicates that the sound gradually decreases until it disappears, like the following version of handleConn:

// gopl.io/ch8/reverb1
func echo(c net.Conn, shout string, delay time.Duration) {
    fmt.Fprintln(c, "\t", strings.ToUpper(stdout))
    time.Sleep(delay)
    fmt.Fprintln(c, "\t", shout)
    time.Sleep(delay)
    fmt.Fprintln(c, "\t", strings.ToLower(shout))
}

func handleConn(c net.Conn) {  // net.Conn is essentially tired of io.Reader
    input := bufio.NewScanner(c)
    for input.Scan() {
        echo(c, input.Text(), 1*time.Second)
    }
    // Note: ignoring potential errors from input.Err()
    c.Close()
}

We need to upgrade our client program so that it can send the input of the terminal to the server and output the return of the server to the terminal, which gives us another good opportunity to use Concurrency:

// gopl.io/ch8/netcat2
func main() {
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    go mustCopy(os.Stdout, conn)
    mustCopy(conn, os.Stdin)
}

When the main goroutine reads the content from the standard input stream and sends it to the server, another goroutine will read and print the response of the server. When the main goroutine encounters input termination, for example, the user presses Control-D(^D) in the terminal and Control-Z on windows, the program will be terminated, although there are tasks in progress in other goroutines. (after introducing channels in 8.4.1, we will understand how to make the program wait for both sides to end).

In the following session, the input of the client is left aligned, and the response of the server will be indented and displayed differently. The client will "shout three times" to the server:

$ go build gopl.io/ch8/reverb1
$ ./reverb1 &
$ go build gopl.io/ch8/netcat2
$ ./netcat2
Hello?
    HELLO?
    Hello?
    hello?
Is there anybody there?
    IS THERE ANYBODY THERE?
Yooo-hooo!
    Is there anybody there?
    is there anybody there?
    YOOO-HOOO!
    Yooo-hooo!
yooo-hooo!
^D
$ killall reverb1

Note that the third shot of the client has not been processed before the previous shot is processed, which seems not particularly "realistic". The echo in the real world should be composed of three shout echoes. In order to simulate the echo of the real world, we need more goroutine s to do this. So we need the keyword go again. This time we use it to call echo:

// gopl.io/ch8/reverb2

func handleConn(c net.Conn) {
    input := bufio.NewScanner(c)
    for input.Scan() {
        go echo(c, input.Text(), 1*time.Second)
    }
    // Note: ignoring potenial errors from input.Err()
    c.Close()
}

The parameters of the function following go will be evaluated when the go statement itself is executed; Therefore, input.Text() will be evaluated in main goroutine. Now the echo is concurrent and will overwrite other responses by time:

$ go build gopl.io/ch8/reverb2
$ ./reverb2 &
$ ./netcat2
Is there anybody there?
    IS THERE ANYBODY THERE?
Yooo-hooo!
    Is there anybody there?
    YOOO-HOOO!
    is there anybody there?
    Yooo-hooo!
    yooo-hooo!
^D
$ killall reverb2

Note to the author: the difference between adding go before echo function is:

  • Without go, the loop waits for echo execution to complete before processing the next input
  • Adding go means that as soon as an input is encountered, a new coroutine is created to run the echo function and wait for the next input in a loop

Let the service use concurrency not only to process the requests of multiple clients, but also to process a single connection, just like the usage of the two go keywords above. However, when we use the go keyword, we need to carefully consider whether the methods in net.Conn are safe when calling concurrently. In fact, they are indeed unsafe for most types. We will explore concurrency security in detail in the next chapter.

Added by ex247 on Tue, 07 Dec 2021 02:24:58 +0200