This is a creation in Article, where the information may have evolved or changed.
A big advantage of the Go language compared to Java is that it is easy to write concurrent programs. The Go language has a built-in goroutine mechanism that uses goroutine to quickly develop concurrent programs and make better use of multicore processor resources. This article studies the application of goroutine and its scheduling implementation.
First, the go language support for concurrency
Programming with Goroutine
Use the Go keyword to create a goroutine. This function is called in the same address space before the go Declaration is placed on a function that needs to be called, so that the function executes as a separate concurrent thread. This thread is called Goroutine in the Go language.
The usage of Goroutine is as follows:
//go 关键字放在方法调用前新建一个 goroutine 并执行方法体go GetThingDone(param1, param2);//新建一个匿名方法并执行go func(param1, param2) {}(val1, val2)//直接新建一个 goroutine 并在 goroutine 中执行代码块go { //do someting...}
Because the goroutine is parallel in a multi-core CPU environment. If a block of code executes in multiple goroutine, we implement code parallelism.
If we need to understand the implementation of the program, how to get parallel results? Need to be used in conjunction with channel.
Controlling Concurrency with Channel
The channels is used to synchronize functions that execute concurrently and to provide a mechanism for some kind of value-sharing communication.
The type of element passed through the channel, the container (or buffer), and the direction of delivery are specified by the "<-" operator.
You can use the built-in function make to assign a channel:
i := make(chan int) // by default the capacity is 0s := make(chan string, 3) // non-zero capacityr := make(<-chan bool) // can only read fromw := make(chan<- []os.FileInfo) // can only write to
Configure Runtime. Gomaxprocs
Use the following code to explicitly set whether to use multi-core to perform concurrent tasks:
runtime.GOMAXPROCS()
The number of Gomaxprocs can be assigned according to the task amount, but not greater than the number of CPU cores.
Configuring parallel execution is a good fit for CPU-intensive, high-parallelism scenarios where IO-intensive use of multicore increases the performance penalty for CPU switching.
Understanding the concurrency mechanism of Go language, next look at the concrete implementation of goroutine mechanism.
Second, the difference between parallel and concurrency
Processes, threads, and processors
In modern operating systems, threads are the basic unit of processor scheduling and allocation, and processes are the basic unit of resource ownership.
Each process consists of a private virtual address space, code, data, and various other system resources. A thread is an execution unit within a process.
Each process has at least one master execution thread, which is created automatically by the system without the user being actively created.
The user creates additional threads in the application as needed, and multiple threads run concurrently in the same process.
Parallelism and concurrency
Parallel and concurrency (Concurrency and Parallelism) are two different concepts, and understanding them is important for understanding multithreaded models.
When describing the concurrency or parallelism of a program, it should be explained from the perspective of the process or thread.
Concurrency: There are a lot of threads or processes executing in a time period, but at any point in time there is only one in execution, multiple threads or processes scrambling for time slices to take turns to execute
Parallelism: There are multiple threads or processes executing at a time and point in time
A non-concurrent program has only one vertical control logic, and at any point in time, the program is only at a certain point in the control logic, which is executed sequentially. If a program is processed at the same time by multiple CPU pipelines at a time, then we say that the program is running in parallel.
Parallel requires hardware support, a single-core processor can only be concurrent, multi-core processors to do parallel execution.
Concurrency is a necessary condition for parallelism, and if a program itself is not concurrent, that is, there is only one logical execution order, then we cannot allow it to be processed in parallel.
Concurrency is not a sufficient condition for parallelism, a concurrent program that is not parallel if it is processed by only one CPU (via ticks).
To give an example, write the simplest program output "Hello World", it is non-concurrent, if you add multiple threads in the program, each thread prints a "Hello World", then this program is concurrent. The runtime only assigns a single CPU to the program, which is not parallel and needs to be run by a multi-core processor's operating system in order to achieve program parallelism.
Three, several different multithreading models
User thread vs. kernel-level thread
Thread implementations can be divided into two categories: User-level threads (User-levelthread, ULT) and kernel-level threads (Kemel-levelthread, KLT). User threads are supported by user code and kernel threads are supported by the operating system kernel.
Multithreaded model
A multithreaded model is a different way of connecting user-level threads to kernel-level threads.
(1) Many-to-one model (M:1)
To map multiple user-level threads to a kernel-level thread, thread management is done in user space.
In this mode, user-level threads are not visible to the operating system (that is, transparent).
Advantages:
The benefit of this model is that thread context switching occurs in user space and avoids modal switching (mode switch), which has a positive effect on performance.
Disadvantage: All threads are based on a kernel scheduler entity that is a kernel thread, which means that only one processor can be exploited, which is not acceptable in a multi-processing environment, in essence, the user thread solves only concurrency problems, but does not solve parallel problems.
If a thread is stuck in a kernel state due to I/O operations, the kernel-state thread blocks waiting for I/O data, all threads will be blocked, user space can use non-blocking and I/O, but there are performance and complexity issues.
(2) One-to-one model (1:1)
Map each user-level thread to a kernel-level thread.
Each thread is dispatched independently by the kernel scheduler, so if one thread is blocked it does not affect the other threads.
Advantage: With the support of multicore processor hardware, the kernel space threading model supports true parallelism, and when one thread is blocked, another thread is allowed to continue execution, so concurrency is strong.
Disadvantage: Each user-level thread created needs to create a kernel-level thread corresponding to it, so the overhead of creating a thread is larger and affects the performance of the application.
(3) Many-to-many models (M:N)
The number of kernel threads and user threads is m:n, and the kernel user space combines the advantages of the first two.
This model requires the kernel thread scheduler and the user space thread scheduler to interoperate, in essence, multiple threads are bound to multiple kernel threads, which makes most of the thread context switching occur in user space, while multiple kernel threads can take advantage of processor resources.
Four, the goroutine mechanism's dispatch realization
The goroutine mechanism implements the M:N threading model, and the goroutine mechanism is an implementation of the co-process (Coroutine), which golang the built-in scheduler that allows each CPU in a multicore CPU to perform a single process.
The key to understanding the principle of goroutine mechanism is to understand the implementation of the Go Language scheduler.
How the Scheduler Works
There are 4 major structures in the go language that support the whole scheduler, namely M, G, P, Sched,
The first three definitions in runtime.h, sched are defined in PROC.C.
The SCHED structure is the scheduler, which maintains queues with storage m and G, as well as some state information of the scheduler.
M structure is machine, system thread, it is managed by the operating system, Goroutine is running above m; M is a large structure that maintains a lot of information such as small object memory cache (Mcache), currently executing goroutine, random number generator, and so on.
P structure is processor, the processor, its main purpose is to perform goroutine, it maintains a goroutine queue, that is, Runqueue. Processor is an important part of dispatching from N:1 to m:n.
G is the core structure of the Goroutine implementation, which contains stacks, instruction pointers, and other information that is important for scheduling goroutine, such as its blocked channel.
The number of processor is set at startup as the value of the environment variable gomaxprocs, or through the run-time call Function Gomaxprocs (). A fixed number of processor means that only Gomaxprocs threads are running the go code at any one time.
Refer to this widely disseminated blog: Http://morsmachine.dk/go-scheduler
We use triangles, rectangles and circles to represent machine processor and goroutine.
In a single-core processor scenario, all goroutine run in the same m system thread, each m system thread maintains a processor, and at any moment, there is only one goroutine in a processor, Other goroutine are waiting in the runqueue. After a Goroutine runs its own time slice, let out the context and return to Runqueue.
In a multi-core processor scenario, each m system thread will hold a processor in order to run Goroutines.
Under normal circumstances, the scheduler will be scheduled according to the above process, but the thread will be blocked and so on, look at the processing of goroutine thread blocking.
Thread blocking
When a running goroutine is blocked, such as making a system call, another system thread (M1) is created, and the current m thread discards its processor,p and goes to the new thread to run.
Runqueue execution Complete
When one of the processor runqueue is empty, no goroutine can be dispatched. It steals half the goroutine from another context.
V. Further thinking on the concurrency realization
The concurrency mechanism of the go language is worth exploring, such as the difference between the go language and Scala concurrency, the contrast between the Golang CSP and the Actor model.
Understanding these implementations of the concurrency mechanism can help us better develop concurrent programs and achieve optimal performance.
About three multithreaded models, you can focus on the implementation of the Java language.
We know that Java encapsulates the differences of the underlying operating system through the JVM, while different operating systems may use different threading models, such as Linux and Windows may use a one-to-one model, and some versions of Solaris and UNIX may use many-to-many models. The JVM specification does not specify a specific implementation of the multithreaded model, either 1:1 (kernel thread), N:1 (user-state thread), m:n (mixed) model. When it comes to the multithreaded model of the Java language, it needs to be implemented for a specific JVM, such as the Oracle/sun hotspot VM, which uses the 1:1 threading model by default.