What is concurrency? What are the concurrency patterns we need to know about? What is the coroutine concurrency model in Go? What is a main goroutine? How is it different from the other Goroutines we’ve enabled ourselves?

This article will answer one by one for you!

▊ concurrent

Serial programs, where the program is executed in the same order as the program was written, have only one context, a stack, a heap.

Concurrent programs need to run multiple contexts, corresponding to multiple call stacks. At runtime, each process has its own call stack and heap, and a complete set of contexts. During invocation, the operating system guarantees the context of the scheduled process. After the process obtains the time, the context of the process is restored to the system.

Serial code is executed line by line and is deterministic, while concurrency introduces uncertainty. Thread communication can only use shared memory. In order to ensure the validity of shared memory, locks can be added, but this introduces the risk of deadlock.

The advantages of concurrency are as follows:

(1) Can make full use of the advantages of the CPU core, improve the execution efficiency of the program.

(2) Concurrency can make full use of the asynchronous nature of CPU and other hardware devices, such as file operations.

Three concurrent modes are described below.

1. Multi-process is a concurrent mode at the operating system level

All processes are managed by the kernel. A process describes the execution of a program. It is a running program.

A process is actually the product of a running program.

Why can a computer run so many applications at once? Why can so many apps refresh in the background at the same time?

This is because multiple processes representing different applications are running simultaneously on top of their operating systems.

The operating system creates a process for each individual program, which can hold the resources needed by the entire program. For example, the program execution progress, execution results, etc., can be placed in it. At the end of the program, destroy the process, and then run the next program, and so on.

Processes are very resource-intensive while the program is running. Once the program is started, it will be loaded into the process, regardless of whether all the resources are used.

The advantage is that the process does not affect each other, the disadvantage is that it is very expensive.

2. Multithreading is a concurrent mode at the system level, and it is also the most used and most effective mode

Threads are within processes and can be thought of as lightweight processes. It can be thought of as the execution flow of code in a process. This allows fewer resources to be used when running the handler and recording intermediate results. When the resource runs out, the thread is destroyed.

Threads are much lighter than processes. A process contains at least one thread. If a process contains only one thread, all the code in it will be executed only serially.

The first threads of each process are created when the process is started, and they are called the main thread of the process to which they belong. Similarly, if there are multiple threads in a process, the code in that process can be executed concurrently.

Except for the first thread of the process, all threads are created by existing threads in the process. That is, any thread other than the main thread can only be explicitly created and destroyed by code. This requires manual control when we write the program.

The advantage is that it is less expensive than the process, but the disadvantage is that it is still more expensive.

3. Goroutine

Essentially, Goroutine is a user-mode thread that does not require preemptive scheduling by the operating system.

In Go programs, the Go language’s runtime system automatically creates and destroys system-level threads.

System-level threads refer to threads provided by the operating system, while user-level threads (goroutines) refer to the flow of code execution that is completely controlled by the user (or the program we write) on top of the system-level threads.

The creation, destruction, scheduling, and state changes of user-level threads, as well as the code and data therein, all need to be implemented and processed by our program itself, which has the following advantages:

(1) Because their creation and destruction do not need to be done by the operating system, they are fast and can improve the task concurrency. Simple programming and clear structure.

(2) It is easy to control and flexible because no operating system is used to schedule their operation.

▊ Coroutine concurrency model

In Go, you don’t communicate by sharing data; rather, you communicate by sharing data.

Go not only has Goroutines, but also a powerful scheduler for scheduling goroutines and connecting to system-level threads.

The scheduler is an important part of the Go language runtime system. It is mainly responsible for coordinating the three major elements of the Go concurrent programming model, namely G (goroutine), P (Processor) and M (Machine).

Where M refers to system-level threads. P refers to an intermediary that can reference several G’s, and can make these G’s docking with M at the appropriate time, and get operation.

Macroscopically, G and M can present a many-to-many relationship due to the existence of P. When a running G that is interfacing with M needs to be paused for an event (such as waiting for I/O or lock release), the scheduler always finds out in time and separates this G from that M to free up computing resources for those gs that are waiting to run.

When G needs to resume running, the scheduler will find free computing resources (including M) for it as soon as possible and schedule it to run. In addition, when M is insufficient, the scheduler requests new system-level threads from the operating system, and when an M is no longer useful, the scheduler is responsible for destroying it in time.

All of the goroutines in the program will be fully scheduled, and the code will run concurrently, even if there are hundreds of thousands of goroutines.

What is a main goroutine? How is it different from the other Goroutines we’ve enabled ourselves?

Let’s start with the following code:

package main

import "fmt"

func main() {
  for i := 0; i < 10; i++ {
    go func() {
      fmt.Println(i)
    }
}
Copy the code

This code just writes a for statement in the main function. The code in this for statement iterates 10 times, with a local variable I representing the sequence number of the iteration, which starts at 0. There is only one Go statement in this for statement, and there is only one Go statement in this Go statement, which calls fmt.Println and wants to print out the value of variable I.

The program is simple, with only three statements. When this program is executed, what does it print?

The answer is: After most computers execute, nothing is printed on the screen.

Why is that?

A process always has a main thread, and similarly, every independent Go program always has a main Goroutine at runtime. This main Goroutine is automatically enabled after the Go program is ready to run.

In general, each Go statement takes a function call, which is the Go function. The Go function of the main Goroutine is the main function that acts as the entry to the program. The Go function executes at a different time from the Go statement to which it belongs.

As shown in the figure below, when a program executes a Go statement, the Go language’s runtime system first attempts to fetch a G (i.e., a goroutine) from one of the free G queues, and only creates a new G if no free G is found.

If a Goroutine already exists, the existing goroutine will always be reused first. If not, start another Goroutine.

In the Go language, the cost of creating G is very low. Creating a G does not have to be done by operating system system calls like creating a process or a system-level thread. Rather, it can be done entirely within the Go language’s runtime system, where a G serves only a context that requires concurrent execution of code fragments.

After getting a free G, the Go runtime wraps the current Go function (or an anonymous function) with the G, and appends the G to some runnable G queue. G in the queue is always arranged by the runtime system in a first-in, first-out order.

Because the preparations described above are unavoidable, they will take time. Therefore, the execution time of the Go function is always slower than that of the Go statement to which it belongs.

With that in mind, let’s look at the example above. Keep in mind that the Go program does not wait for the Go function to execute as soon as the Go statement itself completes. This is asynchronous concurrent execution.

Here “later statement” generally refers to the next iteration of the for statement in the example above. When the last iteration runs, the “after statement” does not exist.

The for statement above completes very quickly. By the time it’s done, the 10 Goroutines that wrapped the Go function often haven’t had a chance to run. The Go call to fmt.Println takes the variable I in the for statement as an argument.

When the for statement completes, none of these Go functions have been executed, so what is the variable I that they refer to?

Once the code in the main Goroutine (that is, the code in the main function) completes, the current Go program finishes running. When the Go program finishes running, no other Goroutine will be executed, whether or not it is running. When the last iteration of the for statement is run, the Go statement is the last statement. So, after executing this Go statement, the code in the main Goroutine is finished, and the Go program ends immediately. So nothing from the previous code will be printed out.

Strictly speaking, Go doesn’t care what order these Goroutines run in. Since the main Goroutine is scheduled along with the other Goroutines we enable ourselves, the scheduler will most likely pause when only part of the code in the Goroutine is executed so that all goroutines have a chance to run. So it’s always unpredictable which goroutine runs first and which goroutine runs after.

For the simple code above, the vast majority of cases are “nothing will be printed”. But for the sake of rigor, “Print ten tens,” “nothing will be printed,” or “print out out-of-order zeros to nines,” are all correct answers.

This principle is very important and I hope the reader can understand it.