golang gmp模型分析
思维导图:

1. 发展过程
思维导图:

在单机时代是没有多线程、多进程、协程这些概念的。早期的操作系统都是顺序执行

单进程的缺点有:
- 单一执行流程、计算机只能一个任务一个任务进行处理
- 进程阻塞所带来的CPU时间的浪费
处于对CPU资源的利用,发展出多线程/多进程操作系统,采用时间片轮训算法

宏观上来说,就算只有一个cpu,也能并发执行多个进程
这样的好处是充分利用了CPU,但是也带来了一些问题,例如时间片切换需要花费额外的开销
- 进程/线程的数量越多,切换
成本就越大,也就越浪费

对于开发人员来说,尽管线程看起来很美好,但实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突等
进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU虽然利用起来了,但如果进程过多,CPU有很大的一部分都被用来进行进程调度了

所以提高cpu的利用率成为我们需要解决的问题
既然问题出现在线程上下文切换中,那么首先我们需要好好想一想什么是线程的上下文切换
我们知道操作系统的一些核心接口是不能被进程随意调度的,例如进行io流的读写操作,需要将最终的执行权交给操作系统(内核态)进行调度,所以就会有用户态和内核态之前的切换

这个时候我们的线程模型是这样的

一个线程需要在内核态与用户态之间进行切换,并且切换是受到操作系统控制的,可能这个现在需要等待多个时间片才能切换到内核态再调用操作系统底层的接口
那么我们是否可以用两个线程分别处理这两种状态呢?两个线程之间再
做好绑定,当用户线程将任务提交给内核线程后,就可以不用堵塞了,可以去执行其他的任务了

对于CPU来说(多核CPU),不需要关注线程切换的问题,只需要分配系统资源给内核线程进行调度即可
我们来给用户线程换个名字——协程(co-runtine)

如果是一比一的关系的话,其实还是可能需要等待内核线程的执行

所以可以设计为N 比 1的形式,多个协程可以将任务一股脑的交给内核线程去完成,但是这样又有问题,如果其中一个问题在提交任务的过程中,堵塞住了,就会影响其他线程的工作

这个就是python的event-loop遇到的问题,一个阻塞,其余全阻塞
所以一般为M 比 N的关系

在M 比 N的关系中,大部分的精力都会放在协程调度器上,如果调度器效率高就能让协程之间阻塞时间尽可能的少
在golang中对
协程调度器和协程内存进行了优化
- 协程调度器:可以支持灵活调度
- 内存轻量化:可以拥有大量的协程

在golang早期的协程调度器中,采用的是队列的方式,M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的
老调度器有几个缺点:
- 创建、销毁、调度G都需要每个M获取锁,这就形成了
激烈的锁竞争 - M转移G会造成
延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G',为了继续执行G,需要把G'交给M'执行,也造成了很差的局部性,因为G'和G是相关的,最好放在M上执行,而不是其他M' - 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销
2. GMP模型设计思想
思维导图:

2.1 GMP模型
GMP是goalng的线程模型,包含三个概念:内核线程(M),goroutine(G),G的上下文环境(P)
- G:
goroutine协程,基于协程建立的用户态线程- M:
machine,它直接关联一个os内核线程,用于执行G- P:
processor处理器,P里面一般会存当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度


在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上
- 全局队列(Global Queue):存放等待运行的
G - P的本地队列:同全局队列类似,存放的也是等待运行的
G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列 - P列表:所有的
P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个 - M:线程想运行任务就得获取
P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去
P和M的数量问题
- P的数量:环境变量
$GOMAXPROCS;在程序中通过runtime.GOMAXPROCS()来设置- M的数量:GO语言本身限定
一万(但是操作系统达不到);通过runtime/debug包中的SetMaxThreads函数来设置;有一个M阻塞,会创建一个新的M;如果有M空闲,那么就会回收或者休眠M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来
2.2 调度器的设计策略
golang调度器的设计策略思想主要有以下几点:
- 复用线程
- 利用并行
- 抢占
- 全局G队列
2.2.1 复用线程
golang在复用线程上主要体现在work stealing机制和hand off机制(偷别人的去执行,和自己扔掉执行)
首先我们看work stealing,我们在学习java的时候学过fork/join,其中也是通过工作窃取方式来提升效率,充分利用线程进行并行计算,并减少了线程间的竞争


干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行
hand off机制
当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行,此时
M1如果长时间阻塞,可能会执行睡眠或销毁

2.2.2 利用并行
我们可以使用GOMAXPROCS设置P的数量,这样的话最多有GOMAXPROCS个线程分布在多个CPU上同时运行。GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = 核数/2,则最多利用了一半的CPU核进行并行
2.2.3 抢占策略
- 1对1模型的调度器,需要等待一个
co-routine主动释放后才能轮到下一个进行使用 - golang中,如果一个
goroutine使用10ms还没执行完,CPU资源就会被其他goroutine所抢占

2.2.4 全局G队列
-
全局G队列其实是复用线程的补充,当工作窃取时,优先从全局队列去取,取不到才从别的p本地队列取(1.17版本)
-
在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G
-
从其他队列的偷取过程是从队列尾部偷取,而队列的执行过程是顺序执行,该队列是双端队列可无锁进行。
-
每次偷取数量为队列一半的量。
2.3 go func()经历了那些过程

- 我们通过
go func()来创建一个goroutine - 有两个存储
G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中 - G只能运行在M中,一个
M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行 - 一个M调度G执行的过程是一个循环机制
- 当M执行某一个G时候如果发生了
syscall或者其他阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P - 当M系统调用结束时候,这个G会尝试获取一个
空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中
2.4 调度器的生命周期
在了解调度器生命周期之前,我们需要了解两个新的角色
M0和G0M0(跟进程数量绑定,一比一):
- 启动程序后
编号为0的主线程- 在全局变量
runtime.m0中,不需要在heap上分配- 负责执行初始化操作和
启动第一个G- 启动第一个G之后,
M0就和其他的M一样了G0(每个M都会有一个G0):
- 每次
启动一个M,都会第一个创建的gourtine,就是G0- G0仅用于
负责调度G- G0不指向任何
可执行的函数- 每个M都会有一个自己的G0
- 在调度或系统调用时会使用M切换到G0,再通过G0进行调度
M0和G0都是放在全局空间的
具体流程为:

我们来分析一段代码:
package mainimport "fmt"func main() {fmt.Println("Hello world")
}
- runtime创建最初的线程m0和goroutine g0,并把2者关联。
- 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个
P构成的P列表。 - 示例代码中的main函数是
main.main,runtime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。 - 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
- G拥有栈,M根据G中的栈信息和调度信息设置运行环境
- M运行G
- G退出,再次回到M获取可运行的G,这样重复下去,直到
main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。
调度器的生命周期几乎占满了一个Go程序的一生,
runtime.main的goroutine执行之前都是为调度器做准备工作,runtime.main的goroutine运行,才是调度器的真正开始,直到runtime.main结束而结束
2.5 协程的主动让渡与抢占
理解

我们已经知道,协程执行time.Sleep时,状态会从_Grunning变为_Gwaiting ,并进入到对应timer中等待,而timer中持有一个回调函数,在指定时间到达后调用这个回调函数,把等在这里的协程恢复到_Grunnable状态,并放回到runq中。

那谁负责在定时器时间到达时,触发定时器注册的回调函数呢?其实每个P都持有一个最小堆,存储在P.timers中,用于管理自己的timer,堆顶timer就是接下来要触发的那一个。

而每次调度时,都会调用checkTimers函数,检查并执行已经到时间的那些timer,不过这还不够稳妥,万一所有M都在忙,不能及时触发调度的话,可能会导致timer执行时间发生较大的偏差。

所以还会通过监控线程来增加一层保障,在介绍HelloGoroutine(GMP一)的执行过程时,我们提过监控线程是由main goroutine创建的,这个监控线程与GMP中的工作线程不同。并不需要依赖P,也不由GMP模型调度,它会重复执行一系列任务,只不过会视情况调整自己的休眠时间。其中一项任务便是保障timer正常执行,监控线程检测到接下来有timer要执行时,不仅会按需调整休眠时间,还会在空不出M时创建新的工作线程,以保障timer可以顺利执行。

当协程等待一个channel时,其状态也会从_Grunnig变成_Gwaiting,并进入到对应的channel的读队列或写队列中等待。

如果协程需要等待IO事件,就也需要让出,以epoll为例,若IO事件尚未就绪,需要注册要等待的IO事件到监听队列中,而每个监听对象都可以关联一个event data。所以就在这里记录是哪个协程在等待,等到事件就绪时再把它恢复到runq中即可 。

不过timer计时器有设置好的触发时间 ,等待的channel可读可写或关闭了,也自会通知到相关协程,而获取就绪的IO时间需要主动轮询,所以为了降低IO延迟,需要时不时的那么轮询一下,也就是执行netpoll。实际上监控线程,调度器,GC等工作过程中都会按需执行netpoll。

全局变量sched中会记录上次netpoll执行的时间,监控线程检测到距离上次轮询已超过了10ms,就会再执行一次netpoll。

上面说的无一例外,都是协程会主动让出的情况,那要是一个协程不会等待timer,channel或者IO事件,就不让出了吗?那必须不能啊,否则调度器岂不成了摆设?那怎么让那些不用等待的协程”让出“呢,这就是监控线程的另一个工作任务了,那就是本着公平调度的原则,对运行时间过长的G,实行”抢占“操作。

就是告诉那些运行时间超过特定阈值(10ms)的G,该让一让了,怎么知道运行时间过长了呢,P里面有一个schedtick字段,每当调度执行一个新的G,并且不继承上个G的时间片时,就会把它自增1,而这个p.sysmontick中,schedwhen记录的是上一次调度的时间,监控线程如果检测到p.sysmontick.schedtick与p.schedtick不相等,说明这个P又发生了新的调度,就会同步这里的调度次数,并更新这个调度时间。

但是若2者相等,就说明自schedwhen这个时间点之后,这个P并未发生新的调度,或者即使发生了新的调度,也延用了之前G的时间片,所以可以通过当前时间与schedwhen的差值来判断当前G是否运行时间过长了。

那如果真的运行时间过长了,要怎么通知它让出呢?这就不得不提到栈增长了,除了对协程栈没什么消耗的函数调用,Go语言编译器都会在函数头部插入栈相关代码。实际上编译器插入的栈增长代码一共有三种。注意这里为什么是”<=“,栈是向下增长的,上面是高地址,下面是低地址

如果栈帧比较小,插入的代码就是这样的,这个SP表示当前协程栈使用到了什么位置,stackguard0是协程栈空间下界,所以当协程栈的消耗达到或超过这个位置时,就需要进行栈增长了。

如果栈帧大小处在_StackSmall和_StackBig之间,插入的代码是这样的,也就是说,当前协程栈使用到这里,若再使用framesize这么多,超出stackguard0的部分大于_StackSmall了,就要进行栈增长了

而对于栈帧大小超过_StackBig的函数,插入的代码就有所有不同了,判断是否要栈增长的方式,本质上同第二种情况相同,而我们要关注的,是这里的stackPreempt ,它是和协程调度相关的重要标识,当runtime希望某个协程让出CPU时,就会把它的stackguard0赋值为stackPreempt。这是一个非常大的值,真正的栈指针不可能指向这个位置,所以可以安全的用作特殊标识。

正因为stackPreempt这个值足够大,所以这两段代码种的判断结果也都会为true,进而跳转到morestack处。

而morestack‘这里,最终会调用runtime.newstack函数,它负责栈增长工作,不过它在进行栈增长之前,会先判断stackguard0是否等于stackPreempt,等于的话就不进行栈增长了,而是执行一次协程调度。

所以在协程不主动让出时,也可以设置stackPreempt标识,通知它让出。

不过这种抢占方式的缺陷,就是过于依赖栈增长代码,如果来个空的for循环,因为与栈增长无关,监控线程等也无法通过设置stackPreempt标识来实现抢占,所以最终导致程序卡死。

这一问题在1.14版本中得到了解决,因为它实现了异步抢占,具体实现在不同平台种不尽相同。例如在Unix平台中,会向协程关联的M发送信号(sigPreempt),接下来目标线程会被信号中断,转去执行runtime.sighandler,在sighandler函数中检测到函数信号为sigPreempt后,就会调用runtime.doSigPreempt函数,它会向当前被打断的协程上下文中,注入一个异步抢占函数调用,处理完信号后sighandler返回,被中断的协程得以恢复,立刻执行被注入的异步抢占函数, 该函数最终会调用runtime中的调度逻辑,这不就让出了嘛。所以在1.14版本中,这段代码执行之前就不会卡死了。

而监控线程的抢占方式又多了一种,异步抢占,其实为了充分利用CPU,监控线程还会抢占处在系统调用中的P,因为一个协程要执行系统调用,就要切换到g0栈,在系统调用没执行完之前,这个M和这个G算是抱团了,不能被分开,也就用不到P,所以在陷入系统调用之前,当前M会让出P,解除m.p与当前p的强关联,只在m.oldp中记录这个p,P的数目毕竟有限,如果有其他协程在等待执行,那么放任P如此闲置就着实浪费了,还是把它关联到其他M继续工作比较划算,不过如果当前M从系统调用中恢复,会先检测之前的P是否被占用,没有的话就继续使用,否则就再去申请一个,没申请到的话,就把当前G放到全局runq中去,然后当前线程m就睡眠了。

说了这么多,不是让出就是抢占。

那让出了,抢占了之后,M也不能闲着,得找到下一个待执行的G来运行,这就是schedule()的职责了。schedul这里要给这个M找到一个待执行的G,首先要确定当前M是否和当前G绑定了,如果绑定了,那当前M就不能执行其他G,所以需要阻塞当前M,等到当前G再次得到调度执行时,自会把当前M唤醒。如果没有绑定,就先看看GC是不是在等待执行,全局变量sched这里,有一个gcwaiting标识,如果GC在等待执行,就去执行GC,回来再继续执行调度程序。接下来还会检查一下有没有要执行的timer。调度程序还有一定几率会去全局runq中获取一部分G到本地runq中。

而获取下一个待执行的G时,会先去本地runq中查找,没有的话,就调用findrunnable(),这个函数直到获取到待运行的G才会返回。在findrunnable()函数这里,也会判断是否要执行GC,然后先尝试从本地runq中获取,没有的话就从全局runq获取一部分,如果还没有,就先尝试执行netpoll,恢复那些IO事件已经就绪了的G,它们会被放回到全局runq中,然后才会尝试从其他P那里steal一些G 。

当调度程序终于获得一个待执行的G以后,还要看看人家有没有绑定的M,如果有的话还得乖乖的把G还给对应的M。而当前M就不得不再次进行调度了。如果没有绑定的M,就调用excute函数在当前M上执行这个G。excute函数这里会简历当前M和这个G的关联关系,并把G的状态从_Grunnable修改为_Grunning,如果不继承上一个执行中协程的时间片,就把P这里的调度计数加一,最后会调用gogo函数,从g.sched这里恢复协程栈指针,指令指针等,接着继续协程的执行。

之前介绍过,协程创建时,会伪装一个执行现场存到g.sched中,所以即使这个G初次执行,也是有一个完美的执行现场的。

现在我们已经知道,协程在某些情况下会主动让出,但有时也需要设置stackPreemt标识,或异步抢占的方式来通知它让出。也了解了调度程序如何获取待执行的G并把它运行起来。期间还穿插介绍了监控线程的主要工作任务”保障计时器正常工作,执行网络轮询,抢占长时间运行的,或处在系统调用的P“,这些都是为了保障程序健康高效的执行,其实监控线程还有一项任务,就是强制执行GC,待到内存管理部分再展开~
2.6 可视化的CMP编程
2.6.1 trace方式
在这里我们需要使用trace编程,三步走:
- 创建trace文件:f, err := os.Create("trace.out")
- 启动trace:err = trace.Start(f)
- 停止trace:trace.Stop()
然后再通过
go tool trace工具打开trace文件go tool trace trace.out
package mainimport ("fmt""os""runtime/trace"
)// trace的编码过程
// 1. 创建文件
// 2. 启动
// 3. 停止
func main() {// 1.创建一个trace文件f, err := os.Create("trace.out")if err != nil {panic(err)}defer func(f *os.File) {err := f.Close()if err != nil {panic(err)}}(f)// 2. 启动traceerr = trace.Start(f)if err != nil {panic(err)}// 正常要调试的业务fmt.Println("hello GMP")// 3. 停止tracetrace.Stop()
}
打开后我们进入网页点击view trace,然后就能看到分析信息

G的信息:

M的信息:

P的信息:

2.6.2 debug方式
使用debug方式可以不需要trace文件
先搞一段代码
package mainimport ("fmt""time"
)func main() {for i := 0; i < 5; i++ {time.Sleep(time.Second)fmt.Println("hello GMP")}
}
debug执行一下
$ GODEBUG=schedtrace=1000 ./debug.exe SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] SCHED 1008ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello GMP SCHED 2009ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello GMP SCHED 3010ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello GMP hello GMP SCHED 4017ms: gomaxprocs=8 idleprocs=8 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0] hello GMP
SCHED:调试信息输出标志字符串,代表本行是goroutine调度器的输出;0ms:即从程序启动到输出这行日志的时间;gomaxprocs: P的数量,本例有2个P, 因为默认的P的属性是和cpu核心数量默认一致,当然也可以通过GOMAXPROCS来设置;idleprocs: 处于idle状态的P的数量;通过gomaxprocs和idleprocs的差值,我们就可知道执行go代码的P的数量;threads: os threads/M的数量,包含scheduler使用的m数量,加上runtime自用的类似sysmon这样的thread的数量;spinningthreads: 处于自旋状态的os thread数量;idlethread: 处于idle状态的os thread的数量;runqueue=0: Scheduler全局队列中G的数量;[0 0]: 分别为2个P的local queue中的G的数量。
参考文章
GolangGMP模型 GMP(三):协程让出,抢占,监控与调度 - cheems~ - 博客园
https://github.com/fengyuan-liang/notes/blob/main/GoLang/golang%E5%A4%A7%E6%9D%80%E5%99%A8GMP%E6%A8%A1%E5%9E%8B.md
相关文章:
golang gmp模型分析
思维导图: 1. 发展过程 思维导图: 在单机时代是没有多线程、多进程、协程这些概念的。早期的操作系统都是顺序执行 单进程的缺点有: 单一执行流程、计算机只能一个任务一个任务进行处理进程阻塞所带来的CPU时间的浪费 处于对CPU资源的利用&…...
深入理解Java Optional:告别NullPointerException的优雅方式
大家好!今天我们来聊聊Java 8引入的一个超实用类 - Optional。不是那个让你重启电脑的CtrlAltDel哦!😄 这是一个能让我们优雅处理null值的工具类,彻底告别烦人的NullPointerException! 一、为什么需要Optional&#x…...
【算法竞赛】树上最长公共路径前缀(蓝桥杯2024真题·团建·超详细解析)
目录 一、题目 二、思路 1. 问题转化:同步DFS走树 2. 优化:同步DFS匹配 3. 状态设计:dfs参数含义 4. 匹配过程:用 map 建立权值索引 5. 终止条件:无法匹配则更新答案 6. 总结 三、完整代码 四、知识点总…...
【windows10】基于SSH反向隧道公网ip端口实现远程桌面
【windows10】基于SSH反向隧道公网ip端口实现远程桌面 1.背景2.SSH反向隧道3.远程连接电脑 1.背景 Windows 10远程桌面协议的简称是RDP(Remote Desktop Protocol)。 RDP是一种网络协议,允许用户远程访问和操作另一台计算机。 远程桌面功…...
Python----概率论与统计(贝叶斯,朴素贝叶斯 )
一、贝叶斯 1.1、贝叶斯定理 贝叶斯定理(Bayes Theorem)也称贝叶斯公式,是关于随机事件的条件概率的定理 贝叶斯的的作用:根据已知的概率来更新事件的概率。 1.2、定理内容 提示: 贝叶斯定理是“由果溯因”的推断&…...
NO.88十六届蓝桥杯备战|动态规划-多重背包|摆花(C++)
多重背包 多重背包问题有两种解法: 按照背包问题的常规分析⽅式,仿照完全背包,第三维枚举使⽤的个数;利⽤⼆进制可以表⽰⼀定范围内整数的性质,转化成01 背包问题。 ⼩建议:并不是所有的多重背包问题都能…...
vue项目打包里面pubilc里的 tinymce里的js文件问题
以下是解决 Vue 项目打包后 public/tinymce 中 JS 文件路径问题的完整方案: 问题原因 当使用 public 目录存放静态资源时,Vue CLI 默认会将 public 下的文件 直接复制到打包目录的根路径,但以下操作可能导致路径错误: 开发环境使…...
Python星球日记 - 第18天:小游戏开发(猜数字游戏)
🌟引言: 上一篇:Python星球日记 - 第17天:数据可视化 名人说:路漫漫其修远兮,吾将上下而求索。(屈原《离骚》) 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、游戏概述与原理1. 游戏基本规则2. 编程知识点3.猜数字游戏流程图二、游戏逻辑设计…...
爬虫抓包工具和PyExeJs模块
我们在处理一些网站的时候, 会遇到一些屏蔽F12, 以及只要按出浏览器的开发者工具就会关闭甚至死机的现象. 在遇到这类网站的时候. 我们可以使用抓包工具把页面上屏蔽开发者工具的代码给干掉. Fiddler和Charles 这两款工具是非常优秀的抓包工具. 他们可以监听到我们计算机上所…...
无人机击落技术难点与要点分析!
一、技术难点 1. 目标探测与识别 小型化和低空飞行:现代无人机体积小、飞行高度低(尤其在城市或复杂地形中),雷达和光学传感器难以有效探测。 隐身技术:部分高端无人机采用吸波材料或低可探测设计,进…...
2025年Java无服务器架构实战:AWS Lambda与Spring Cloud Function深度整合
摘要 📝 本文深入探讨如何在2025年Java生态中实现AWS Lambda与Spring Cloud Function的无缝整合。我们将从基础概念讲起,逐步深入到实际部署、性能优化和最佳实践,通过详实的代码示例展示如何构建高效、可扩展的无服务器Java应用。 目录 &a…...
LeetCode 题目 「二叉树的右视图」 中,如何从「中间存储」到「一步到位」实现代码的优化?
背景简介 在 LeetCode 的经典题目 「二叉树的右视图」 中,我们需要返回从右侧看一棵二叉树时所能看到的节点集合。每一层我们只能看到最右边的那个节点。 最初,我采用了一个常规思路:层序遍历 每层单独保存节点值 最后提取每层最后一个节…...
8.第二阶段x64游戏实战-string类
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 本次游戏没法给 内容参考于:微尘网络安全 上一个内容:7.第二阶段x64游戏实战-分析人物属性 string类是字符串类,在计算机中…...
Go语言sync.Mutex包源码解读
互斥锁sync.Mutex是在并发程序中对共享资源进行访问控制的主要手段,对此Go语言提供了非常简单易用的机制。sync.Mutex为结构体类型,对外暴露Lock()、Unlock()、TryLock()三种方法,分别用于阻塞加锁、解锁、非阻塞加锁操作(加锁失败…...
C++实现文件断点续传:原理剖析与实战指南
文件传输示意图 一、断点续传的核心价值 1.1 大文件传输的痛点分析 网络闪断导致重复传输:平均重试3-5次。 传输进度不可回溯:用户无法查看历史进度。 带宽利用率低下:每次中断需从头开始。 1.2 断点续传技术优势 指标传统传输断点续传…...
MySQL中FIND_IN_SET函数与INSTR函数用法解析
一、功能定义与语法 1、FIND_IN_SET函数 语法:FIND_IN_SET(str, strlist) 功能:在逗号分隔的字符串列表(strlist)中查找精确匹配的子字符串(str),并返回其位置(从1开始)…...
Python贝叶斯回归、强化学习分析医疗健康数据拟合截断删失数据与参数估计3实例
全文链接:https://tecdat.cn/?p41391 在当今数据驱动的时代,数据科学家面临着处理各种复杂数据和构建有效模型的挑战。本专题合集聚焦于有序分类变量处理、截断与删失数据回归分析以及强化学习模型拟合等多个重要且具有挑战性的数据分析场景,…...
Git 协同开发的常用操作
1. 单仓库(多分支开发) 从远程拉取代码 git clone https://gitee.com/...查看当前分支 git branch -- *master创建并切换到你的开发分支(my-dev) git checkout -b my-dev查看当前分支 git branch -- marster -- *my-dev提交代…...
微信小程序 -- 原生封装table
文章目录 table.wxmltable.wxss注意 table.js注意 结果数据结构 最近菜鸟做微信小程序的一个查询功能,需要展示excel里面的数据,但是菜鸟找了一圈,也没发现什么组件库有table,毕竟手机端好像确实不太适合做table! 菜鸟…...
分布式文件存储系统FastDFS
文章目录 1 分布式文件存储1_分布式文件存储的由来2_常见的分布式存储框架 2 FastDFS介绍3 FastDFS安装1_拉取镜像文件2_构建Tracker服务3_构建Storage服务4_测试图片上传 4 客户端操作1_Fastdfs-java-client2_文件上传3_文件下载4_获取文件信息5_问题 5 SpringBoot整合 1 分布…...
ZKmall开源商城服务端验证:Jakarta Validation 详解
ZKmall开源商城基于Spring Boot 3构建,其服务端数据验证采用Jakarta Validation API(原JSR 380规范),通过声明式注解与自定义扩展机制实现高效、灵活的数据校验体系。以下从技术实现、核心能力、场景优化三个维度展开解析&#…...
深度分页及优化建议
深度分页的定义 深度分页是指在分页查询中,当用户请求非常靠后的页面时,数据库需要处理大量数据,导致查询性能显著下降的情况。例如,一个查询结果有 100 万条记录,而用户要查询第 999 页(每页 10 条记录&a…...
电网电能质量分析:原理、算法及实际应用
一、引言 在现代社会,电力供应的稳定性和可靠性对工业生产、社会生活的各个方面都至关重要。电能质量作为衡量电力系统供电能力的关键指标,其优劣直接影响到电力设备的运行效率、使用寿命以及生产过程的稳定性。随着电力系统规模的不断扩大,新…...
学透Spring Boot — 017. 魔术师—Http消息转换器
本文是我的专栏《学透Spring Boot》的第17篇文章,了解更多请移步我的专栏: 学透 Spring Boot_postnull咖啡的博客-CSDN博客 目录 HTTP请求和响应 需求—新的Media Type 实现—新的Media Type 定义转换器 注册转换器 编写Controller 测试新的medi…...
BOE(京东方)旗下控股子公司“京东方能源”成功挂牌新三板 以科技赋能零碳未来
2025年4月8日,BOE(京东方)旗下控股子公司京东方能源科技股份有限公司(以下简称“京东方能源”)正式通过全国中小企业股份转让系统审核,成功在新三板挂牌(证券简称:能源科技,证券代码:874526),成为BOE(京东方)自物联网转型以来首个独立孵化并成功挂牌的子公司。此次挂牌是BOE(京…...
Airflow集成Lark机器人
🥭1. 实现目标 🕐 通过自定义函数,实现Lark机器人告警功能 🕐 通过Lark机器人代替邮件数据的发送功能 🥭2.自定义函数实现 from airflow import DAG from airflow.operators.python_operator import PythonOperator from airflow.models import Variable import requ…...
Git使用与管理
一.基本操作 1.创建本地仓库 在对应文件目录下进行: git init 输入完上面的代码,所在文件目录下就会多一个名为 .git 的隐藏文件,该文件是Git用来跟踪和管理仓库的。 我们可以使用 tree 命令(注意要先下载tree插件)…...
计算机网络——传输层(Udp)
udp UDP(User Datagram Protocol,用户数据报协议 )是一种无连接的传输层协议,它在IP协议(互联网协议)之上工作,为应用程序提供了一种发送和接收数据报的基本方式。以下是UDP原理的详细解释&…...
网络安全小知识课堂(五)
病毒与蠕虫:你的电脑为何会 “生病” 和 “传染”? 引言 你是否见过这样的场景:电脑突然弹窗广告暴增,文件莫名消失,甚至整个公司网络集体瘫痪?这些症状背后,可能是 ** 病毒(Virus…...
图解Java设计模式
1、设计模式面试题 2、设计模式的重要性 3、7大设计原则介绍 3.1、单一职责原则...

