Go language high concurrency series 1: basic theory

A core strength of the Go language is its concurrency capabilities. However, in actual work, there are many small partners who cannot master the concurrency and goroutine control of Go language proficiently, resulting in some performance problems and serious bugs sometimes.
So I am going to write four articles to systematically sort out the knowledge related to Go language concurrency. While reviewing it myself, I can also consolidate it for other small partners in the company. By the way, I will introduce some concurrency controllers I encapsulated for the convenience of other small partners. use.

Without further ado, let's get to the point.
I plan to write a total of four articles to sort out the core knowledge of Go language concurrency:

  1. basic theory
  2. goroutine basics
  3. context
  4. concurrent mode

process

definition of process

A process is a running activity of a program in a computer on a data set, the basic unit of resource allocation and scheduling in the system, and the basis of the operating system structure. In the early computer structure of process-oriented design, the process is the basic execution entity of the program; in the contemporary computer structure of thread-oriented design, the process is the container of the thread. A program is a description of instructions, data, and their organization, and a process is the entity of a program

process pool

For example, when PHP is doing back-end development, it generally adopts the architecture of NGINX + PHP-FPM when deploying. This architecture is concurrency in the form of a process pool.

The process pool generally consists of two parts: master and worker

The master process mainly manages the reception of event signals and assignment of tasks, creates new worker processes, and recycles idle worker processes
. Worker processes are industrious workers who work hard on the tasks assigned by the master.

This concurrency method is simple and rude + stable and reliable. The processes are naturally isolated from each other, and the crash of one process will not affect the operation of other processes.
However, the creation and destruction of processes will consume more system resources, and the communication between processes will be more troublesome.

thread

Definition of thread

A thread is the smallest unit that the operating system can schedule, and in most cases, it is contained in a process. A thread refers to a single sequential control flow in a process. A process can have multiple threads concurrently, and each thread executes different tasks concurrently.

同一进程中的多条线程将共享该进程中的全部系统资源,比如虚拟地址空间、文件描述符、信号处理等。但是同一进程的多条线程有各自的调用栈、寄存器、线程本地存储。

线程池

线程过多会带来额外的开销,包括创建和销毁线程的开销,调度线程的开销等。所以采用多线程并发时,一般会使用线程池来维护多个进程,等待任务分配。

线程池的实现一般是生产者消费者模型,由生产者、任务队列、消费者三部分组成。 生产者:生产工作任务并存入任务队列 消费者:从任务队列取出任务进行处理,如果任务队列没有任务,就进入挂起状态

线程池的数量控制一般由两步分组成,核心线程和非核心线程。核心线程在线程池启动时就进行创建,并且长期存在,无限期的等待任务。非核心线程则是当任务数量超出核心线程负荷时,进行创建,如果执行完任务后一段时间内没有新的任务,就回收掉。

线程控制主要需要注意:

  • 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
  • 销毁太多线程,将导致之后浪费时间再次创建它们。

协程

协程的定义

协程,英文叫作 Coroutine,协程是一种用户态的轻量级线程。

协程拥有自己的上下文和栈,协程调度切换的时候,将寄存器上下文和栈保存下来,在切回来的时候,又恢复之前保存的上下文和栈。所以协程可以保留上一次执行时的状态,即局部状态的一个特定组合,每次切换回来时就相当于进入上次调用的状态。

协程本质上是在单线程内运行,在线程内进行切换。

阻塞与非阻塞

阻塞:阻塞状态是指程序未得到所需计算资源时,被挂起的状态,程序在等待某个操作完成,自身无法继续处理其他事情。
常见的阻塞有:网络IO阻塞,磁盘IO阻塞等 非阻塞:非阻塞是指程序在等待期间,仍然可以继续处理其他事情。阻塞操作会导致耗时和效率低下,变成非阻塞一般可以提高效率。

同步与异步

同步:不同的程序单元为了完成某个任务,有序的依次执行,称为同步执行。比如更新数据库的一条数据,对行加锁使得不同的更新请求依次排队执行,就是同步操作。
异步:为了完成某个任务,不同的程序单元之间无需协同,一个程序单元的执行不需要等待另一个程序单元执行完毕,就是异步操作。
比如程序执行的操作日志,当程序需要记录日志时,可以将日志信息丢给日志程序去保存,而它本身不需要等待日志程序保存完成,就可以继续执行其他操作了。

小结

我们到现在为止了解了大量的概念,其实这些概念的出现,都是为了提高效率,为了更高效的利用系统资源。 为了更高效的利用系统资源,我们就要让程序尽量以非阻塞的形式异步运行。

例如:我们服务器向外发送一个http请求,这之间会有较长一段时间的网络IO阻塞。这个时候我们就可以把它挂起,去做其他的事情,等到响应返回,再来继续处理。
这种情况下程序就以异步非阻塞的方式运行,大大提高了执行效率,充分利用了cpu的计算能力。

goroutine

现在开始讲我们今天的重点:goroutine。 goroutine是Go语言的一种机制,是Go语言中协程的实现。Go语言会智能地将goroutine中的任务合理的分配给cpu,因为它在语言层面已经内置了调度和切换上下文的机制。
在Go语言中,当我们需要某个任务并发执行的时候,只需要将任务包装成一个函数,开启一个goroutine去执行就可以了。

goroutine的栈在生命周期开始的时候只有很小的容量,一般情况下是2kb。但goroutine栈的大小是不固定的,可以根据需要增大和缩小。所以在Go语言中一次创建十万的goroutine也是没问题的。

GPM

GPM是Go语言自己实现的一套调度系统

image.png 首先有一个全局队列,里面存放着待运行的goroutine

  • P 管理着一组goroutine队列,容量有限,只能放256个。新建G时会优先放入P的队列,如果队列满了就会把队列的一半放入全局队列。
  • P 里面会存储当前G的上下文环境(函数指针、堆栈地址、地址边界),P会对自己管理的G队列进些调度,当自己队列消费完了,就去全局队列里取。如果全局队列也消费完了,就去其他P里抢任务。
  • M 是Go的runtime对操作系统内核线程的虚拟,M与内核线程一般一一对应。P管理的G会在M上运行,然后OS调度器会把内核线程分配到CPU执行。

P的个数

  • P的数量由启动时的$GOMAXPROCS环境变量,或者由runtime的方法GOMAXPROCS()决定。
  • Go1.5以后默认的GOMAXPROCS数量是物理线程数

M的个数

  • M的最大数量限制默认是10000,但是内核一般很难支持这么多。
  • runtime/debug中的SetMaxThreads函数,可以设置M的最大数量
  • M的数量是不固定的,当一个M阻塞住,P就会换一个M或者创建一个新的M。所以就算P的数量是1,也可能会创建很多个M。

GPM可视化

要查看GPM的运行情况,我们可以通过: go tool trace
下面让我们来简单测试一下:

package main

import (
   "os"
   "runtime/trace"
   "sync"
)

func run(wg *sync.WaitGroup) {
   wg.Add(1)
   go gofun(wg)
}

func gofun(wg *sync.WaitGroup) {
   defer wg.Done()

   n := 1
   for i := 0; i < 100000; i++ {
      n += 1
   }

}

func main() {
   f, err := os.Create("trace.out")
   if err != nil {
      panic(err)
   }
   defer f.Close()

   err = trace.Start(f)
   if err != nil {
      panic(err)
   }
   defer trace.Stop()

   var wg sync.WaitGroup

   run(&wg)
   run(&wg)
   run(&wg)
   run(&wg)

   wg.Wait()
}
复制代码

首先运行程序:

$ go run main.go 
复制代码

此时会得到一个trace.out文件,我们用go tool trace 来打开它

$ go tool trace trace.out       
2022/04/12 11:12:58 Parsing trace...
2022/04/12 11:12:58 Splitting trace...
2022/04/12 11:12:58 Opening browser. Trace viewer is listening on http://127.0.0.1:51019
复制代码

现在我们打开浏览器,访问http://127.0.0.1:51019 ,点击view trace就可以看到我们刚才运行程序时,GPM的调度情况了。

概览

image.png

G信息

点击Goroutines这一行,任一时间段,就可以看到详细信息。
比如我们现在点击100μs这个时间:

image.png

image.png 详细信息里,第一行是一个特殊的G,是每个M必须有的一个初始化的G,这个我们不必讨论。
我们可以看到有3个G正在运行中

P信息

P这里我们可以看到,一共有6个G被3个P分别调度运行 image.png

M信息

M这里我们可以看到,随着P的调度,M的数量会增加和减少 image.png

总结

从线程调度上来讲,一般语言是由OS内核直接进行线程调度,而Go的gorountine是有Go自己的调度器进行调度,这种调度是在用户态完成的,不涉及内核的频繁切换。
包括内存的分配,也是在用户态维护者一块大的内存池,不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。
另一方面充分利用了多核的硬件资源,近似把若干goroutine均分到物理线程上。 再加上本身goroutine也很轻量,各方面来讲保证了Go的并发性能。

Guess you like

Origin juejin.im/post/7085598310915899429