Golang——GPM调度器

ops/2025/1/15 11:03:05/

本文详细介绍Golang的GPM调度器,包括底层源码及其实现,以及一些相关的补充知识。

在这里插入图片描述

文章目录

    • 前情提要
      • 并发与并行
        • 并行 (Parallel)
        • 并发 (Concurrency)
        • 关键区别
      • 进程和线程的区别
      • 协程
        • 解决的问题
        • 协程的优势
    • Go的并发模型-CSP
    • Go的调度模型-GPM源码
      • Goroutine
        • g 结构体
        • stack结构体
        • gobuf结构体
        • schedt结构体
      • Processor
        • p结构体
        • 重要的全局变量
      • Machine
        • M 和 G 的关系
        • M 和 P 的关系
    • Go的调度模型-流程
      • PM绑定
      • 创建G并加入P的私有队列
      • P的私有队列满,加入G对应的全局队列
      • M通过P消费G
      • 没有可执行的G,M和P断开绑定
      • 其他情况
        • G被阻塞
        • P随M进入系统调用
    • 监控线程`sysmon`
    • `runtime` 包及相关功能
      • `runtime.Gosched()`
      • `runtime.Goexit()`
      • `runtime.GOMAXPROCS(n int)`
        • `runtime.NumGoroutine()`
    • GMP 限制分析
      • M 的限制
      • G 的限制
      • P 的限制

前情提要

并发与并行

并行 (Parallel)
  • 定义:
    在同一时刻,多条指令在多个处理器上同时执行。
    • 宏观: 看起来是一起执行的。
    • 微观: 实际上多个任务同时进行。
并发 (Concurrency)
  • 定义:
    在同一时刻只能有一条指令执行,但多个进程指令快速轮换执行,宏观上看像是多个任务同时进行。
    • 宏观: 多个进程同时执行。
    • 微观: 单一时间段内交替执行。
关键区别
  • 并行强调在 同一时刻,多个任务在不同的处理器上执行。
  • 并发强调在 同一时间段内,多个任务交替执行,给人一种同时进行的假象。

进程和线程的区别

  1. 资源分配和调度单位

    • 进程: 是资源分配的最小单位。
    • 线程: 是程序执行的最小单位(资源调度的最小单位)。
  2. 地址空间

    • 进程: 有独立的地址空间。
      • 启动一个进程需要分配地址空间和维护段表,开销较大。
    • 线程: 共享进程的地址空间,切换和创建的开销更小。
  3. 通信方式

    • 线程: 共享地址空间,直接访问全局变量等数据。可以通过共享内存、同步机制(互斥锁(Mutex)、读写锁(RWMutex)、信号量(Semaphore))等进行通信。
    • 进程:
      • 同一进程内的线程可以直接共享全局变量,通信方便。
      • 不同进程需要使用 IPC(如管道、共享内存、消息队列、套接字(Socket))进行通信
  4. 健壮性

    • 进程: 独立运行,一个进程崩溃不会影响其他进程。
    • 线程: 多线程中任意一个线程崩溃会导致整个进程终止。

协程

协程是“用户态的轻量级线程”,协程的调度完全由用户控制,不为操作系统所知的,它由编程语言层面实现,上下文切换不需要经过内核态,再加上协程占用的内存空间极小,所以有着非常大的发展潜力。

解决的问题

主要用来解决操作系统线程太“重”的问题,所谓的太重,主要表现在以下两个方面:

  1. 创建和切换的高开销
    • 系统线程创建和切换需要进入内核,开销较大。
  2. 内存使用浪费
    • 系统线程栈空间较大,且一旦创建不可动态缩减或扩展。
协程的优势
  1. 轻量化

    • goroutine 是用户态线程,创建和切换无需进入内核。
    • 开销远小于系统线程。
  2. 灵活的栈内存管理

    • 启动时栈大小为 2KB
    • 栈可以根据需要自动扩展和收缩。
  3. 高并发能力

    • goroutine 支持创建成千上万甚至上百万的协程并发执行,性能和内存开销较低。

接下来讲解Go到并发调度——GPM调度器


Go的并发模型-CSP

常见的并发模型有七种:

  1. 线程与锁
  2. 函数式编程
  3. Clojure之道
  4. actor
  5. 通讯顺序进程(CSP)
  6. 数据级并行
  7. Lambda架构

Go 语言的并发模型是通信顺序进程(Communicating Sequential Processes,CSP) 的范型(paradigm),核心观念是将两个并发执行的实体通过通道channel连接起来,所有的消息都通过channel传输。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在goroutine之间同步和传递数据的关键数据类型叫作通道(channel)。

channel详情请参考:【TODO】


Go的调度模型-GPM源码

GPM代表了三个角色,分别是Goroutine、Processor、Machine。下面详细介绍他们。

  • Goroutine:协程,用go关键字创建的执行体,它对应一个结构体g,结构体里保存了goroutine的堆栈信息。

  • Machine:表示操作系统的线程。

  • Processor:表示处理器,管理G和M的联系。

Goroutine

Goroutine就是代码中使用go关键词创建的执行单元,也是大家熟知的有“轻量级线程”之称的协程,协程是不为操作系统所知的,它由编程语言层面实现,上下文切换不需要经过内核态,再加上协程占用的内存空间极小,所以有着非常大的发展潜力。

  • goroutine建立在操作系统线程基础之上,协程与操作系统线程之间是多对多(M:N)的关系。
  • 这里的 M:N 是指M个goroutine运行在N个操作系统线程之上,内核负责调度这N个操作系统线程;N个系统线程又负责调度这M个goroutine。
  • 所谓的对goroutine的调度,是指程序代码按照一定的算法在适当的时候挑选出合适的goroutine并放到CPU上去运行的过程,这些负责对goroutine进行调度的程序代码我们称之为goroutine调度器。

在Go语言中,声明一个Goroutine很简单。如下:

go">go func() {}()

为了实现对goroutine的调度,使用 g 结构体来保存CPU寄存器的值以及goroutine的其它一些状态信息。

g 结构体保存了goroutine的所有信息,调度器代码可以通过 g 对象来对goroutine进行调度:

  • goroutine被调离CPU时,调度器代码负责把CPU寄存器的值保存在g对象的成员变量之中
  • goroutine被调度起来运行时,调度器代码又负责把g对象的成员变量所保存的寄存器的值恢复到CPU的寄存器。
g 结构体

g结构体用于代表一个goroutine,该结构体保存了goroutine的所有信息,包括栈,gobuf结构体和其它的一些状态信息:

go">// 前文所说的g结构体,它代表了一个goroutine
type g struct {// Stack parameters.// stack describes the actual stack memory: [stack.lo, stack.hi).// stackguard0 is the stack pointer compared in the Go stack growth prologue.// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.// stackguard1 is the stack pointer compared in the C stack growth prologue.// It is stack.lo+StackGuard on g0 and gsignal stacks.// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).// 记录该goroutine使用的栈stack       stack   // offset known to runtime/cgo// 下面两个成员用于栈溢出检查,实现栈的自动伸缩,抢占调度也会用到stackguard0stackguard0 uintptr // offset known to liblinkstackguard1 uintptr // offset known to liblink......// 此goroutine正在被哪个工作线程执行m              *m      // current m; offset known to arm liblink// 保存调度信息,主要是几个寄存器的值sched          gobufstktopsp      uintptr  // 期望 sp 位于栈顶,用于回溯检查param         unsafe.Pointer // wakeup 唤醒时候传递的参数......// schedlink字段指向全局运行队列中的下一个g,//所有位于全局运行队列中的g形成一个链表schedlink      guintptr......// 抢占调度标志,如果需要抢占调度,设置preempt为truepreempt        bool       // preemption signal, duplicates stackguard0 = stackpreempt......}

g结构体中两个主要的子结构体(stack和gobuf)介绍如下:

stack结构体

stack结构体主要用来记录goroutine所使用的栈的信息,包括栈顶和栈底位置:

go">// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
//用于记录goroutine使用的栈的起始和结束位置
type stack struct {  lo uintptr   // 栈顶,指向内存低地址hi uintptr   // 栈底,指向内存高地址
}
gobuf_177">gobuf结构体

gobuf结构体用于保存goroutine的调度信息,主要包括CPU的几个寄存器的值:

go">type gobuf struct {// The offsets of sp, pc, and g are known to (hard-coded in) libmach.//// ctxt is unusual with respect to GC: it may be a// heap-allocated funcval, so GC needs to track it, but it// needs to be set and cleared from assembly, where it's// difficult to have write barriers. However, ctxt is really a// saved, live register, and we only ever exchange it between// the real register and the gobuf. Hence, we treat it as a// root during stack scanning, which means assembly that saves// and restores it doesn't need write barriers. It's still// typed as a pointer so that any other writes from Go get// write barriers.sp   uintptr  // 保存CPU的rsp寄存器的值pc   uintptr  // 保存CPU的rip寄存器的值g    guintptr // 记录当前这个gobuf对象属于哪个goroutinectxt unsafe.Pointer// 保存系统调用的返回值,因为从系统调用返回之后如果p被其它工作线程抢占,// 则这个goroutine会被放入全局运行队列被其它工作线程调度,其它线程需要知道系统调用的返回值。ret  sys.Uintreg  lr   uintptr// 保存CPU的rip寄存器的值bp   uintptr  // for GOEXPERIMENT=framepointer
}

要实现对goroutine的调度,仅仅有g结构体对象是不够的,至少还需要一个存放所有(可运行)goroutine的容器,便于工作线程寻找需要被调度起来运行的goroutine。
Go调度器引入了schedt结构体,

  • 保存调度器自身的状态信息;
  • 保存goroutine的全局运行队列。

schedt结构体

schedt结构体用来保存调度器的状态信息和goroutine的全局运行队列

schedt结构体被定义成了一个共享的全局变量,这样每个工作线程都可以访问它以及它所拥有的goroutine运行队列,我们称这个运行队列为全局运行队列。

go">type schedt struct {// accessed atomically. keep at top to ensure alignment on 32-bit systems.goidgen  uint64lastpoll uint64lock mutex// When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be// sure to call checkdead().// 由空闲的工作线程组成链表midle        muintptr // idle m's waiting for work// 空闲的工作线程的数量nmidle       int32    // number of idle m's waiting for worknmidlelocked int32    // number of locked m's waiting for workmnext        int64    // number of m's that have been created and next M ID// 最多只能创建maxmcount个工作线程maxmcount    int32    // maximum number of m's allowed (or die)nmsys        int32    // number of system m's not counted for deadlocknmfreed      int64    // cumulative number of freed m'sngsys uint32 // number of system goroutines; updated atomically// 由空闲的p结构体对象组成的链表pidle      puintptr // idle p's// 空闲的p结构体对象的数量npidle     uint32nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.// Global runnable queue.// goroutine全局运行队列runq     gQueuerunqsize int32......// Global cache of dead G's.// gFree是所有已经退出的goroutine对应的g结构体对象组成的链表// 用于缓存g结构体对象,避免每次创建goroutine时都重新分配内存gFree struct {lock          mutexstack        gList // Gs with stacksnoStack   gList // Gs without stacksn              int32}......
}

因为全局运行队列是每个工作线程都可以读写的,因此访问它需要加锁,然而在一个繁忙的系统中,加锁会导致严重的性能问题。

  • 调度器为每个工作线程引入了一个私有的局部goroutine运行队列,工作线程优先使用自己的局部运行队列,只有必要时才会去访问全局运行队列,这大大减少了锁冲突,提高了工作线程的并发性。

  • 在Go调度器源代码中,局部运行队列被包含在p结构体的实例对象之中,参考下面Proccessor中的p结构体。

Processor

Proccessor负责Machine与Goroutine的连接,它的作用如下:

  • 它能提供线程需要的上下文环境,
  • 分配G到它应该去的线程上执行。(局部goroutine运行队列被包含在p结构体)
p结构体

同样的,处理器的数量也是默认按照GOMAXPROCS来设置的,与线程的数量一一对应。主要存储

  • 性能追踪、垃圾回收、计时器等相关的字段外
  • 处理器的待待执行的局部goroutine运行队列。
go">type p struct {lock mutexstatus       uint32 // one of pidle/prunning/...link            puintptrschedtick   uint32     // incremented on every scheduler callsyscalltick  uint32     // incremented on every system callsysmontick  sysmontick // last tick observed by sysmonm                muintptr   // back-link to associated m (nil if idle)......// Queue of runnable goroutines. Accessed without lock.//本地goroutine运行队列runqhead uint32  // 队列头runqtail uint32     // 队列尾runq     [256]guintptr  //使用数组实现的循环队列// runnext, if non-nil, is a runnable G that was ready'd by// the current G and should be run next instead of what's in// runq if there's time remaining in the running G's time// slice. It will inherit the time left in the current time// slice. If a set of goroutines is locked in a// communicate-and-wait pattern, this schedules that set as a// unit and eliminates the (potentially large) scheduling// latency that otherwise arises from adding the ready'd// goroutines to the end of the run queue.runnext guintptr// Available G's (status == Gdead)gFree struct {gListn int32}......
}
重要的全局变量
go">allgs     []*g     // 保存所有的g
allm       *m    // 所有的m构成的一个链表,包括下面的m0
allp       []*p    // 保存所有的p,len(allp) == gomaxprocsncpu             int32   // 系统中cpu核的数量,程序启动时由runtime代码初始化
gomaxprocs int32   // p的最大值,默认等于ncpu,但可以通过GOMAXPROCS修改sched      schedt     // 调度器结构体对象,记录了调度器的工作状态m0  m       // 代表进程的主线程
g0   g        // m0的g0,也就是m0.g0 = &g0
  • 全局变量的初始状态:
    • 切片(allgsallp)初始化为空。
    • 指针(allm)初始化为 nil
    • 整数变量(如 ncpugomaxprocs)初始化为 0
    • 结构体(如 sched)的所有成员初始化为对应类型的零值。
  • 程序启动时:
    • 这些全局变量逐步被赋值和初始化,表示当前程序的调度器状态、线程信息和可用的处理器。

Machine

M就是对应操作系统的线程

  • 最多有 GOMAXPROCS 个活跃线程(M)同时运行,默认情况下 GOMAXPROCS 的值等于 CPU 核心数。
  • 每个 M 对应一个 runtime.m 结构体实例。

为什么线程数等于 CPU 核数?
每个线程分配到一个 CPU 核心,可以避免线程的上下文切换,从而减少系统开销,提高性能。

m 结构体用来代表工作线程,它保存了g p m 三方的信息:

  1. m 自身使用的栈信息
  2. 当前 m 上正在运行的 goroutine
  3. m 绑定的 p 的信息等

详见下面定义中的注释:

go">type m struct {// g0主要用来记录工作线程使用的栈信息,在执行调度代码时需要使用这个栈// 执行用户goroutine代码时,使用用户goroutine自己的栈,调度时会发生栈的切换g0      *g     // goroutine with scheduling stack// 通过TLS实现m结构体对象与工作线程之间的绑定tls           [6]uintptr   // thread-local storage (for x86 extern register)mstartfn      func()// 指向工作线程正在运行的goroutine的g结构体对象curg          *g       // current running goroutine// 记录与当前工作线程绑定的p结构体对象p             puintptr // attached p for executing go code (nil if not executing go code)nextp         puintptroldp          puintptr // the p that was attached before executing a syscall// spinning状态:表示当前工作线程正在试图从其它工作线程的本地运行队列偷取goroutinespinning      bool // m is out of work and is actively looking for workblocked       bool // m is blocked on a note// 没有goroutine需要运行时,工作线程睡眠在这个park成员上,// 其它线程通过这个park唤醒该工作线程park          note// 记录所有工作线程的一个链表alllink       *m // on allmschedlink     muintptr// Linux平台thread的值就是操作系统线程IDthread        uintptr // thread handlefreelink      *m      // on sched.freem......
}

M里面存了两个比较重要的东西,一个是g0,一个是curg。

M 和 G 的关系
  1. g0
  • 保存 m 使用的调度栈,负责运行时任务的管理,与用户 goroutine 栈分离。
  • 调度代码运行时使用 g0 的栈,而用户代码运行时使用用户 goroutine 的栈。
  • 主要用于 goroutine 的创建、内存分配、任务切换等运行时调度操作。
  1. curg
    • 当前线程正在运行的用户 goroutine
    • 每个线程在同一时刻只能运行一个 goroutinecurg 指向该任务。

M 和 P 的关系
  1. p

    • 当前线程正在运行的处理器。
    • 提供执行 Go 代码所需的上下文资源,比如本地运行队列和内存分配缓存。
  2. nextp

    • 暂存处理器,通常用于 M 在需要切换任务时暂存 P。
  3. oldp

    • 系统调用之前的处理器,用于在系统调用结束后恢复原处理器环境。

Go的调度模型-流程

PM绑定

默认启动GOMAXPROCS(四)个线程GOMAXPROCS(四)个处理器,然后互相绑定。
在这里插入图片描述

创建G并加入P的私有队列

一旦G被创建,在进行栈信息和寄存器等信息以及调度相关属性更新之后,它就要进到一个P的队列等待发车。
在这里插入图片描述
创建多个G,轮流往其他P的私有队列里面放。
在这里插入图片描述

P的私有队列满,加入G对应的全局队列

假如有很多G,都塞满了,那就把G塞到全局队列里(候车大厅)。

在这里插入图片描述

M通过P消费G

除了往里塞之外,M这边还要疯狂往外取:

  • 首先去处理器的私有队列里取G执行;
  • 如果取完的话就去全局队列取;
  • 如果全局队列里也没有的话,就去其他处理器队列里偷。
    在这里插入图片描述

没有可执行的G,M和P断开绑定

如果哪里都没找到要执行的G,那M就会和P断开关系,然后去睡觉(idle)了。
在这里插入图片描述

其他情况

G被阻塞

如果两个Goroutine正在通过channel执行任务时阻塞,PM会与G立刻断开,找新的G继续执行。
在这里插入图片描述

P随M进入系统调用

如果G进行了系统调用syscall,M也会跟着进入系统调用状态,那么这个P留在这里就浪费了,P不会等待G和M系统调用完成,而是P与GM立刻断开,找其他比较闲的M执行其他的G。
在这里插入图片描述
当G完成了系统调用,因为要继续往下执行,所以必须要再找一个空闲的处理器发车。
在这里插入图片描述
如果没有空闲的处理器了,那就只能把G放回全局队列当中等待分配。
在这里插入图片描述

监控线程sysmon

sysmon 是 Go 运行时中的一个特殊线程(M),也被称为 监控线程。就像 保洁阿姨,定时清理系统中“不干活”的资源,确保调度器的正常运行和系统的高效运转。

  • 特点:

    • 不需要 P(处理器)即可独立运行。
    • 20 微秒到 10 毫秒 被唤醒一次,频率动态调整,执行系统级的维护任务。
  • 主要职责

    1. 垃圾回收

      • 执行垃圾回收相关的辅助任务,释放不再使用的内存资源。
    2. 回收长时间阻塞的 P

      • 当某个 P 长时间处于系统调用或其他阻塞状态时,sysmon 会将其从 M 中解绑,使资源得到重新利用。
    3. 发起抢占调度

      • 监控长时间运行的 Ggoroutine),如果某个 G 运行时间过长,sysmon 会发出抢占信号,强制切换到其他 G
      • 确保调度的公平性,防止某个 goroutine 独占资源。

      想在程序中实现抢占,可以使用: runtime.Gosched()

runtime 包及相关功能

runtime.Gosched()

  • 作用:
    • 将当前的 goroutine 暂停,让出 CPU 时间片,让调度器切换其他的 goroutine 执行。
    • 当前的 goroutine 会在稍后重新进入运行状态。
  • 使用场景:
    • 实现协作式的任务切换,避免单个 goroutine 长时间占用 CPU。

runtime.Goexit()

  • 作用:
    • 终止当前 goroutine,并执行该 goroutinedefer 语句。
  • 注意事项:
    • 不会影响其他 goroutine 或整个程序的运行,仅结束调用它的 goroutine
    • 用于提前退出某些任务。

runtime.GOMAXPROCS(n int)

  • 作用:
    • 设置 Go 运行时使用的最大 CPU 核心数。
    • 返回之前设置的值。
  • 默认值:
    • Go 1.5 之前,默认使用单核。
    • Go 1.5 及之后,默认使用所有 CPU 核心数。
  • 作用解释:
    • 决定同时运行的系统线程(M)的数量,对调度性能有直接影响。
  • 使用场景:
    • 限制程序对 CPU 核心的使用,避免过度竞争。

runtime.NumGoroutine()
  • 作用:
    • 返回当前程序启动的 goroutine 数量。
  • 使用场景:
    • 用于监控 goroutine 的数量,帮助判断程序是否有潜在的内存或调度问题。

GMP 限制分析

M 的限制

  • M 是什么:
    • M 代表操作系统线程,最终执行所有的任务。
  • 限制:
    • 默认最大数量为 10000
    • 超过限制会报错:GO: runtime: program exceeds 10000-thread limit
  • 异常场景:
    • 通常只有当大量 goroutine 阻塞时,才会触发这种限制。
  • 调整方法:
    • 使用 debug.SetMaxThreads 增加限制。

G 的限制

  • G 是什么:
    • G 代表 goroutine,是 Go 的轻量级线程。
  • 限制:
    • 没有硬性限制,理论上可以创建任意数量的 goroutine
    • 实际数量受 内存大小 的限制。
  • 内存占用:
    • 每个 goroutine 大约占用 2~4 KB 的连续内存块。
  • 注意事项:
    • 申请的 goroutine过多,如果内存不足,可能会导致创建失败或系统崩溃。当然 这种情况出现在 Goroutine 数量非常庞大(如数百万)。几十个或几百个 Goroutine: 一般是合理的,具体需根据任务性质决定。

P 的限制

  • P 是什么:
    • P 代表调度器中的处理器,负责连接 G 和 M。
  • 限制:
    • 数量由 GOMAXPROCS 参数决定。
    • 默认值等于 CPU 核心数。
  • 影响:
    • P 的数量影响任务并行执行的程度,但不影响 goroutine 的创建数量。
  • 合理设置:
    • 一般情况下,设置为机器的 CPU 核数即可。


http://www.ppmy.cn/ops/150269.html

相关文章

开发人员学习书籍推荐(C#、Python方向)

作为一名开发人员,持续学习和提升自己的技术水平是至关重要的。如今,技术不断更新换代,新的开发框架、语言和工具层出不穷。对于刚入行的开发者或希望深入某一领域的工程师来说,选对书籍是学习的捷径之一。本篇文章将推荐一些经典…

C#调用MyLibxl来生成EXCEL的订货清单

在进销存里,基本上都有销售订单, 而这些订单的格式更是五花八门的。 一般情况用EXCEL的文件就可以表达出来,然后再通过打印EXCEL文件,就完成了整个订单的生成了。 下面就来生成如下面所示的销售收据: 接着需要编写下面这段代码: using MyLibxl; using MyLib.Libxl; u…

Apache Hop从入门到精通 第三课 Apache Hop下载安装

1、下载 官方下载地址:https://hop.apache.org/download/,本教程是基于apache-hop-client-2.11.0.zip进行解压,需要jdk17,小伙伴们可以根据自己的需求下载相应的版本。如下图所示 2、下载jdk17(https://www.microsoft…

移动云自研云原生数据库入围国采!

近日,中央国家机关2024年度事务型数据库软件框架协议联合征集采购项目产品名单正式公布,移动云自主研发的云原生数据库产品顺利入围。这一成就不仅彰显了移动云在数据库领域深耕多年造就的领先技术优势,更标志着国家权威评审机构对移动云在数…

【微服务】SpringBoot 自定义消息转换器使用详解

目录 一、前言 二、SpringBoot 内容协商介绍 2.1 什么是内容协商 2.2 内容协商机制深入理解 2.2.1 内容协商产生的场景 2.3 内容协商实现的常用方式 2.3.1 前置准备 2.3.2 通过HTTP请求头 2.3.2.1 操作示例 2.3.3 通过请求参数 三、SpringBoot 消息转换器介绍 3.1 H…

python 爬虫自动获取 GB/T 7714 引用格式

python 自动获取 GB/T 7714 引用格式 参考:python爬虫实现自动获取论文GB 7714引用 介绍:从 Google Scholar 网站(具体为 https://xueshu.aigrogu.com/)收集文章信息,包括文章标题、链接和 GB/T 7714 引用格式。该代码…

vue 与 vue-json-viewer 实现 JSON 数据可视化

前言 接口的调试和测试是确保系统稳定性的重要步骤。为了让开发人员和测试人员能够直观地查看接口返回的 JSON 数据,使用合适的工具至关重要。vue-json-viewer 插件为 vue 开发者提供了一个简单而强大的解决方案。本文将详细介绍如何在 vue 项目中使用该插件&#x…

QT 键值对集合QMap

在QT中&#xff0c;可以使用QMap作为键值对的集合。QMap是Qt的一个模板类&#xff0c;它存储了键值对&#xff0c;并且可以通过键来快速查找值。 导入 #include <QMap> 以下是一些使用QMap的方法&#xff1a; 1.创建并初始化一个 QMap<int, QString> UserDepa…