【Go】-调度器简介
目录
数据结构
G
M
P
调度器启动
创建 Goroutine
初始化结构体
运行队列
调度信息
调度循环
小结
数据结构
Go的运行时调度器的三个重要组成部分 — 线程 M、Goroutine G 和处理器 P:
图 6-29 Go 语言调度器
- G — 表示 Goroutine,它是一个待执行的任务;
- M — 表示操作系统的线程,它由操作系统的调度器调度和管理;
- P — 表示处理器,它可以被看做运行在线程上的本地调度器;
G
Goroutine 是 Go 语言调度器中待执行的任务,它在运行时调度器中的地位与线程在操作系统中差不多,但是它占用了更小的内存空间,也降低了上下文切换的开销。
Goroutine 只存在于 Go 语言的运行时,它是 Go 语言在用户态提供的线程,作为一种粒度更细的资源调度单元,如果使用得当能够在高并发的场景下更高效地利用机器的 CPU。
Goroutine 在 Go 语言运行时使用私有结构体 runtime.g 表示。这个私有结构体非常复杂,这里也不会介绍所有的字段,仅会挑选其中的一部分,首先是与栈相关的两个字段:
type g struct {stack stackstackguard0 uintptr
}
其中 stack
字段描述了当前 Goroutine 的栈内存范围 [stack.lo, stack.hi),另一个字段 stackguard0
可以用于调度器抢占式调度。除了 stackguard0
之外,Goroutine 中还包含另外三个与抢占密切相关的字段:
type g struct {preempt bool // 抢占信号preemptStop bool // 抢占时将状态修改成 `_Gpreempted`preemptShrink bool // 在同步安全点收缩栈
}
最后,我们再节选一些比较重要的字段:
type g struct {m *msched gobufatomicstatus uint32goid int64
}
m
— 当前 Goroutine 占用的线程,可能为空;atomicstatus
— Goroutine 的状态;sched
— 存储 Goroutine 的调度相关的数据;goid
— Goroutine 的 ID,该字段对开发者不可见,Go 团队认为引入 ID 会让部分 Goroutine 变得更特殊,从而限制语言的并发能力10;
上述四个字段中,我们需要展开介绍 sched
字段的 runtime.gobuf 结构体中包含哪些内容:
type gobuf struct {sp uintptrpc uintptrg guintptrret sys.Uintreg...
}
sp
— 栈指针;pc
— 程序计数器;g
— 持有 runtime.gobuf 的 Goroutine;ret
— 系统调用的返回值;
这些内容会在调度器保存或者恢复上下文的时候用到,其中的栈指针和程序计数器会用来存储或者恢复寄存器中的值,改变程序即将执行的代码。
结构体 runtime.g 的 atomicstatus
字段存储了当前 Goroutine 的状态。除了几个已经不被使用的以及与 GC 相关的状态之外,Goroutine 可能处于以下 9 种状态:
状态 | 描述 |
---|---|
_Gidle | 刚刚被分配并且还没有被初始化 |
_Grunnable | 没有执行代码,没有栈的所有权,存储在运行队列中 |
_Grunning | 可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P |
_Gsyscall | 正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上 |
_Gwaiting | 由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上 |
_Gdead | 没有被使用,没有执行代码,可能有分配的栈 |
_Gcopystack | 栈正在被拷贝,没有执行代码,不在运行队列上 |
_Gpreempted | 由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒 |
_Gscan | GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在 |
上述状态中比较常见是 _Grunnable
、_Grunning
、_Gsyscall
、_Gwaiting
和 _Gpreempted
五个状态,这里会重点介绍这几个状态。Goroutine 的状态迁移是个复杂的过程,触发 Goroutine 状态迁移的方法也很多,在这里我们也没有办法介绍全部的迁移路线,只会从中选择一些介绍。
虽然 Goroutine 在运行时中定义的状态非常多而且复杂,但是我们可以将这些不同的状态聚合成三种:等待中、可运行、运行中,运行期间会在这三种状态来回切换:
- 等待中:Goroutine 正在等待某些条件满足,例如:系统调用结束等,包括
_Gwaiting
、_Gsyscall
和_Gpreempted
几个状态; - 可运行:Goroutine 已经准备就绪,可以在线程运行,如果当前程序中有非常多的 Goroutine,每个 Goroutine 就可能会等待更多的时间,即
_Grunnable
; - 运行中:Goroutine 正在某个线程上运行,即
_Grunning
;
上图展示了 Goroutine 状态迁移的常见路径,其中包括创建 Goroutine 到 Goroutine 被执行、触发系统调用或者抢占式调度器的状态迁移过程。
M
Go 语言并发模型中的 M 是操作系统线程。调度器最多可以创建 10000 个线程,但是其中大多数的线程都不会执行用户代码(可能陷入系统调用),最多只会有 GOMAXPROCS
个活跃线程能够正常运行。
在默认情况下,运行时会将 GOMAXPROCS
设置成当前机器的核数,我们也可以在程序中使用 runtime.GOMAXPROCS 来改变最大的活跃线程数。
在默认情况下,一个四核机器会创建四个活跃的操作系统线程,每一个线程都对应一个运行时中的 runtime.m 结构体。
在大多数情况下,我们都会使用 Go 的默认设置,也就是线程数等于 CPU 数,默认的设置不会频繁触发操作系统的线程调度和上下文切换,所有的调度都会发生在用户态,由 Go 语言调度器触发,能够减少很多额外开销。
Go 语言会使用私有结构体 runtime.m 表示操作系统线程,这个结构体也包含了几十个字段,这里先来了解几个与 Goroutine 相关的字段:
type m struct {g0 *gcurg *g...
}
其中 g0 是持有调度栈的 Goroutine,curg
是在当前线程上运行的用户 Goroutine,这也是操作系统线程唯一关心的两个 Goroutine。
g0 是一个运行时中比较特殊的 Goroutine,它会深度参与运行时的调度过程,包括 Goroutine 的创建、大内存分配和 CGO 函数的执行。在后面的小节中,我们会经常看到 g0 的身影。
runtime.m 结构体中还存在三个与处理器相关的字段,它们分别表示正在运行代码的处理器 p
、暂存的处理器 nextp
和执行系统调用之前使用线程的处理器 oldp
:
type m struct {p puintptrnextp puintptroldp puintptr
}
除了在上面介绍的字段之外,runtime.m 还包含大量与线程状态、锁、调度、系统调用有关的字段,我们会在分析调度过程时详细介绍它们。
P
调度器中的处理器 P 是线程和 Goroutine 的中间层,它能提供线程需要的上下文环境,也会负责调度线程上的等待队列,通过处理器 P 的调度,每一个内核线程都能够执行多个 Goroutine,它能在 Goroutine 进行一些 I/O 操作时及时让出计算资源,提高线程的利用率。
因为调度器在启动时就会创建 GOMAXPROCS
个处理器,所以 Go 语言程序的处理器数量一定会等于 GOMAXPROCS
,这些处理器会绑定到不同的内核线程上。
runtime.p 是处理器的运行时表示,作为调度器的内部实现,它包含的字段也非常多,其中包括与性能追踪、垃圾回收和计时器相关的字段,这些字段也非常重要,但是在这里就不展示了,我们主要关注处理器中的线程和运行队列:
type p struct {m muintptrrunqhead uint32runqtail uint32runq [256]guintptrrunnext guintptr...
}
反向存储的线程维护着线程与处理器之间的关系,而 runqhead
、runqtail
和 runq
三个字段表示处理器持有的运行队列,其中存储着待执行的 Goroutine 列表,runnext
中是线程下一个需要执行的 Goroutine。
runtime.p 结构体中的状态 status
字段会是以下五种中的一种:
状态 | 描述 |
---|---|
_Pidle | 处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空 |
_Prunning | 被线程 M 持有,并且正在执行用户代码或者调度器 |
_Psyscall | 没有执行用户代码,当前线程陷入系统调用 |
_Pgcstop | 被线程 M 持有,当前处理器由于垃圾回收被停止 |
_Pdead | 当前处理器已经不被使用 |
通过分析处理器 P 的状态,我们能够对处理器的工作过程有一些简单理解,例如处理器在执行用户代码时会处于 _Prunning
状态,在当前线程执行 I/O 操作时会陷入 _Psyscall
状态。
调度器启动
调度器的启动过程是我们平时比较难以接触的过程,不过作为程序启动前的准备工作,理解调度器的启动过程对我们理解调度器的实现原理很有帮助,运行时通过 runtime.schedinit 初始化调度器:
func schedinit() {_g_ := getg()...sched.maxmcount = 10000...sched.lastpoll = uint64(nanotime())procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}if procresize(procs) != nil {throw("unknown runnable goroutine during bootstrap")}
}
在调度器初始函数执行的过程中会将 maxmcount
设置成 10000,这也就是一个 Go 语言程序能够创建的最大线程数,虽然最多可以创建 10000 个线程,但是可以同时运行的线程还是由 GOMAXPROCS
变量控制。
我们从环境变量 GOMAXPROCS
获取了程序能够同时运行的最大处理器数之后就会调用 runtime.procresize 更新程序中处理器的数量,在这时整个程序不会执行任何用户 Goroutine,调度器也会进入锁定状态,runtime.procresize 的执行过程如下:
- 如果全局变量
allp
切片中的处理器数量少于期望数量,会对切片进行扩容; - 使用
new
创建新的处理器结构体并调用 runtime.p.init 初始化刚刚扩容的处理器; - 通过指针将线程 m0 和处理器
allp[0]
绑定到一起; - 调用 runtime.p.destroy 释放不再使用的处理器结构;
- 通过截断改变全局变量
allp
的长度保证与期望处理器数量相等; - 将除
allp[0]
之外的处理器 P 全部设置成_Pidle
并加入到全局的空闲队列中;
调用 runtime.procresize 是调度器启动的最后一步,在这一步过后调度器会完成相应数量处理器的启动,等待用户创建运行新的 Goroutine 并为 Goroutine 调度处理器资源。
创建 Goroutine
想要启动一个新的 Goroutine 来执行任务时,我们需要使用 Go 语言的 go
关键字,编译器会通过 cmd/compile/internal/gc.state.stmt 和 cmd/compile/internal/gc.state.call 两个方法将该关键字转换成 runtime.newproc 函数调用:
func (s *state) call(n *Node, k callKind) *ssa.Value {if k == callDeferStack {...} else {switch {case k == callGo:call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, newproc, s.mem())default:}}...
}
runtime.newproc 的入参是参数大小和表示函数的指针 funcval
,它会获取 Goroutine 以及调用方的程序计数器,然后调用 runtime.newproc1 函数获取新的 Goroutine 结构体、将其加入处理器的运行队列并在满足条件时调用 runtime.wakep 唤醒新的处理执行 Goroutine:
func newproc(siz int32, fn *funcval) {argp := add(unsafe.Pointer(&fn), sys.PtrSize)gp := getg()pc := getcallerpc()systemstack(func() {newg := newproc1(fn, argp, siz, gp, pc)_p_ := getg().m.p.ptr()runqput(_p_, newg, true)if mainStarted {wakep()}})
}
runtime.newproc1 会根据传入参数初始化一个 g
结构体,我们可以将该函数分成以下几个部分介绍它的实现:
- 获取或者创建新的 Goroutine 结构体;
- 将传入的参数移到 Goroutine 的栈上;
- 更新 Goroutine 调度相关的属性;
首先是 Goroutine 结构体的创建过程:
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) *g {_g_ := getg()siz := nargsiz = (siz + 7) &^ 7_p_ := _g_.m.p.ptr()newg := gfget(_p_)if newg == nil {newg = malg(_StackMin)casgstatus(newg, _Gidle, _Gdead)allgadd(newg)}...
上述代码会先从处理器的 gFree
列表中查找空闲的 Goroutine,如果不存在空闲的 Goroutine,会通过 runtime.malg 创建一个栈大小足够的新结构体。
接下来,我们会调用 runtime.memmove 将 fn
函数的所有参数拷贝到栈上,argp
和 narg
分别是参数的内存空间和大小,我们在该方法中会将参数对应的内存空间整块拷贝到栈上:
...totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSizetotalSize += -totalSize & (sys.SpAlign - 1)sp := newg.stack.hi - totalSizespArg := spif narg > 0 {memmove(unsafe.Pointer(spArg), argp, uintptr(narg))}...
拷贝了栈上的参数之后,runtime.newproc1 会设置新的 Goroutine 结构体的参数,包括栈指针、程序计数器并更新其状态到 _Grunnable
并返回:
...memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))newg.sched.sp = spnewg.stktopsp = spnewg.sched.pc = funcPC(goexit) + sys.PCQuantumnewg.sched.g = guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched, fn)newg.gopc = callerpcnewg.startpc = fn.fncasgstatus(newg, _Gdead, _Grunnable)newg.goid = int64(_p_.goidcache)_p_.goidcache++return newg
}
我们在分析 runtime.newproc 的过程中,保留了主干省略了用于获取结构体的 runtime.gfget、runtime.malg、将 Goroutine 加入运行队列的 runtime.runqput 以及设置调度信息的过程,下面会依次分析这些函数。
初始化结构体
runtime.gfget 通过两种不同的方式获取新的 runtime.g:
- 从 Goroutine 所在处理器的
gFree
列表或者调度器的sched.gFree
列表中获取 runtime.g; - 调用 runtime.malg 生成一个新的 runtime.g 并将结构体追加到全局的 Goroutine 列表
allgs
中。
runtime.gfget 中包含两部分逻辑,它会根据处理器中 gFree
列表中 Goroutine 的数量做出不同的决策:
- 当处理器的 Goroutine 列表为空时,会将调度器持有的空闲 Goroutine 转移到当前处理器上,直到
gFree
列表中的 Goroutine 数量达到 32; - 当处理器的 Goroutine 数量充足时,会从列表头部返回一个新的 Goroutine;
func gfget(_p_ *p) *g {
retry:if _p_.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {for _p_.gFree.n < 32 {gp := sched.gFree.stack.pop()if gp == nil {gp = sched.gFree.noStack.pop()if gp == nil {break}}_p_.gFree.push(gp)}goto retry}gp := _p_.gFree.pop()if gp == nil {return nil}return gp
}
当调度器的 gFree
和处理器的 gFree
列表都不存在结构体时,运行时会调用 runtime.malg 初始化新的 runtime.g 结构,如果申请的堆栈大小大于 0,这里会通过 runtime.stackalloc 分配 2KB 的栈空间:
func malg(stacksize int32) *g {newg := new(g)if stacksize >= 0 {stacksize = round2(_StackSystem + stacksize)newg.stack = stackalloc(uint32(stacksize))newg.stackguard0 = newg.stack.lo + _StackGuardnewg.stackguard1 = ^uintptr(0)}return newg
}
runtime.malg 返回的 Goroutine 会存储到全局变量 allgs
中。
简单总结一下,runtime.newproc1 会从处理器或者调度器的缓存中获取新的结构体,也可以调用 runtime.malg 函数创建。
运行队列
runtime.runqput 会将 Goroutine 放到运行队列上,这既可能是全局的运行队列,也可能是处理器本地的运行队列:
func runqput(_p_ *p, gp *g, next bool) {if next {retryNext:oldnext := _p_.runnextif !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {goto retryNext}if oldnext == 0 {return}gp = oldnext.ptr()}
retry:h := atomic.LoadAcq(&_p_.runqhead)t := _p_.runqtailif t-h < uint32(len(_p_.runq)) {_p_.runq[t%uint32(len(_p_.runq))].set(gp)atomic.StoreRel(&_p_.runqtail, t+1)return}if runqputslow(_p_, gp, h, t) {return}goto retry
}
- 当
next
为true
时,将 Goroutine 设置到处理器的runnext
作为下一个处理器执行的任务; - 当
next
为false
并且本地运行队列还有剩余空间时,将 Goroutine 加入处理器持有的本地运行队列; - 当处理器的本地运行队列已经没有剩余空间时就会把本地队列中的一部分 Goroutine 和待加入的 Goroutine 通过 runtime.runqputslow 添加到调度器持有的全局运行队列上;
处理器本地的运行队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。
简单总结一下,Go 语言有两个运行队列,其中一个是处理器本地的运行队列,另一个是调度器持有的全局运行队列,只有在本地运行队列没有剩余空间时才会使用全局队列。
调度信息
运行时创建 Goroutine 时会通过下面的代码设置调度相关的信息,前两行代码会分别将程序计数器和 Goroutine 设置成 runtime.goexit 和新创建 Goroutine 运行的函数:
...newg.sched.pc = funcPC(goexit) + sys.PCQuantumnewg.sched.g = guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched, fn)...
上述调度信息 sched
不是初始化后的 Goroutine 的最终结果,它还需要经过 runtime.gostartcallfn 和 runtime.gostartcall 的处理:
func gostartcallfn(gobuf *gobuf, fv *funcval) {gostartcall(gobuf, unsafe.Pointer(fv.fn), unsafe.Pointer(fv))
}func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {sp := buf.spif sys.RegSize > sys.PtrSize {sp -= sys.PtrSize*(*uintptr)(unsafe.Pointer(sp)) = 0}sp -= sys.PtrSize*(*uintptr)(unsafe.Pointer(sp)) = buf.pcbuf.sp = spbuf.pc = uintptr(fn)buf.ctxt = ctxt
}
调度信息的 sp
中存储了 runtime.goexit 函数的程序计数器,而 pc
中存储了传入函数的程序计数器。因为 pc
寄存器的作用就是存储程序接下来运行的位置,所以 pc
的使用比较好理解,但是 sp
中存储的 runtime.goexit 会让人感到困惑,我们需要配合下面的调度循环来理解它的作用。
调度循环
调度器启动之后,Go 语言运行时会调用 runtime.mstart 以及 runtime.mstart1,前者会初始化 g0 的 stackguard0
和 stackguard1
字段,后者会初始化线程并调用 runtime.schedule 进入调度循环:
func schedule() {_g_ := getg()top:var gp *gvar inheritTime boolif gp == nil {if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {lock(&sched.lock)gp = globrunqget(_g_.m.p.ptr(), 1)unlock(&sched.lock)}}if gp == nil {gp, inheritTime = runqget(_g_.m.p.ptr())}if gp == nil {gp, inheritTime = findrunnable()}execute(gp, inheritTime)
}
runtime.schedule 函数会从下面几个地方查找待执行的 Goroutine:
- 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过
schedtick
保证有一定几率会从全局的运行队列中查找对应的 Goroutine; - 从处理器本地的运行队列中查找待执行的 Goroutine;
- 如果前两种方法都没有找到 Goroutine,会通过 runtime.findrunnable 进行阻塞地查找 Goroutine;
runtime.findrunnable 的实现非常复杂,这个 300 多行的函数通过以下的过程获取可运行的 Goroutine:
- 从本地运行队列、全局运行队列中查找;
- 从网络轮询器中查找是否有 Goroutine 等待运行;
- 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的 Goroutine,该函数还可能窃取处理器的计时器;
因为函数的实现过于复杂,上述的执行过程是经过简化的,总而言之,当前函数一定会返回一个可执行的 Goroutine,如果当前不存在就会阻塞等待。
接下来由 runtime.execute 执行获取的 Goroutine,做好准备工作后,它会通过 runtime.gogo 将 Goroutine 调度到当前线程上。
func execute(gp *g, inheritTime bool) {_g_ := getg()_g_.m.curg = gpgp.m = _g_.mcasgstatus(gp, _Grunnable, _Grunning)gp.waitsince = 0gp.preempt = falsegp.stackguard0 = gp.stack.lo + _StackGuardif !inheritTime {_g_.m.p.ptr().schedtick++}gogo(&gp.sched)
}
runtime.gogo 在不同处理器架构上的实现都不同,但是也都大同小异,下面是该函数在 386 架构上的实现:
TEXT runtime·gogo(SB), NOSPLIT, $8-4MOVL buf+0(FP), BX // 获取调度信息MOVL gobuf_g(BX), DXMOVL 0(DX), CX // 保证 Goroutine 不为空get_tls(CX)MOVL DX, g(CX)MOVL gobuf_sp(BX), SP // 将 runtime.goexit 函数的 PC 恢复到 SP 中MOVL gobuf_ret(BX), AXMOVL gobuf_ctxt(BX), DXMOVL $0, gobuf_sp(BX)MOVL $0, gobuf_ret(BX)MOVL $0, gobuf_ctxt(BX)MOVL gobuf_pc(BX), BX // 获取待执行函数的程序计数器JMP BX // 开始执行
它从 runtime.gobuf 中取出了 runtime.goexit 的程序计数器和待执行函数的程序计数器,其中:
- runtime.goexit 的程序计数器被放到了栈 SP 上;
- 待执行函数的程序计数器被放到了寄存器 BX 上;
在函数调用一节中,我们曾经介绍过 Go 语言的调用惯例,正常的函数调用都会使用 CALL
指令,该指令会将调用方的返回地址加入栈寄存器 SP 中,然后跳转到目标函数;当目标函数返回后,会从栈中查找调用的地址并跳转回调用方继续执行剩下的代码。
runtime.gogo 就利用了 Go 语言的调用惯例成功模拟这一调用过程,通过以下几个关键指令模拟 CALL
的过程:
MOVL gobuf_sp(BX), SP // 将 runtime.goexit 函数的 PC 恢复到 SP 中MOVL gobuf_pc(BX), BX // 获取待执行函数的程序计数器JMP BX // 开始执行
上图展示了调用 JMP
指令后的栈中数据,当 Goroutine 中运行的函数返回时,程序会跳转到 runtime.goexit 所在位置执行该函数:
TEXT runtime·goexit(SB),NOSPLIT,$0-0CALL runtime·goexit1(SB)func goexit1() {mcall(goexit0)
}
经过一系列复杂的函数调用,我们最终在当前线程的 g0 的栈上调用 runtime.goexit0 函数,该函数会将 Goroutine 转换会 _Gdead
状态、清理其中的字段、移除 Goroutine 和线程的关联并调用 runtime.gfput 重新加入处理器的 Goroutine 空闲列表 gFree
:
func goexit0(gp *g) {_g_ := getg()casgstatus(gp, _Grunning, _Gdead)gp.m = nil...gp.param = nilgp.labels = nilgp.timer = nildropg()gfput(_g_.m.p.ptr(), gp)schedule()
}
在最后 runtime.goexit0 会重新调用 runtime.schedule 触发新一轮的 Goroutine 调度,Go 语言中的运行时调度循环会从 runtime.schedule 开始,最终又回到 runtime.schedule,我们可以认为调度循环永远都不会返回。
这里介绍的是 Goroutine 正常执行并退出的逻辑,实际情况会复杂得多,多数情况下 Goroutine 在执行的过程中都会经历协作式或者抢占式调度,它会让出线程的使用权等待调度器的唤醒。
小结
Goroutine 和调度器是 Go 语言能够高效地处理任务并且最大化利用资源的基础,本节介绍了 Go 语言用于处理并发任务的 G - M - P 模型,我们不仅介绍了它们各自的数据结构以及常见状态,还通过特定场景介绍调度器的工作原理以及不同数据结构之间的协作关系,相信能够帮助各位读者理解调度器的实现。
相关文章:
【Go】-调度器简介
目录 数据结构 G M P 调度器启动 创建 Goroutine 初始化结构体 运行队列 调度信息 调度循环 小结 数据结构 Go的运行时调度器的三个重要组成部分 — 线程 M、Goroutine G 和处理器 P: 图 6-29 Go 语言调度器 G — 表示 Goroutine,它是一个待…...
在Ubuntu 22.04上设置Python 3的Jupyter Notebook
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 简介 Jupyter Notebook 是一个作为 Web 应用程序的交互式计算命令 shell。该工具可与多种语言一起使用,包括 Python、Julia…...
通讯专题4.1——CAN通信之计算机网络与现场总线
从通讯专题4开始,来学习CAN总线的内容。 为了更好的学习CAN,先从计算机网络与现场总线开始了解。 1 计算机网络体系的结构 在我们生活当中,有许多的网络,如交通网(铁路、公路等)、通信网(电信、…...
将jar包导入maven
1.将jar包放repository 2.执行命令:mvn install:install-file -DgroupIdcom.oracle -DartifactIdojdbc7 -Dversion12.1.0.2 -Dpackagingjar -DfileD:\dev\utils\idea\repository\ojdbc7.jar -Dfile: 指定要安装的JAR文件的路径。 -DgroupId: 指定项目的groupId。 -…...
Mysql实现定时自动备份(Windows环境)
一.新建数据库备份目录 二.新建批处理文件 创建批处理文件mysql_backup.bat echo off set BACKUP_DIRD:\backup set MYSQL_USERroot set MYSQL_PASS123456 set MYSQL_HOSTlocalhost set DATABASE_NAMEphoenix set DATE%date:~0,4%-%date:~5,2%-%date:~8,2%_%time:~0,2%-%time…...
kafka数据在服务端时怎么写入的
学习背景 接着上篇,我们来聊聊kafka数据在服务端怎么写入的 服务端写入 在介绍服务端的写流程之前,我们先要理解服务端的几个角色之间的关系。 假设我们有一个由3个broker组成的kafka集群,我们在这个集群上创建一个topic叫做shitu-topic&…...
2024算法基础公选课练习七(BFS1)
一、前言 还是偏基础的bfs,但是有几个题不是很好写 二、题目总览 三、具体题目 3.1 问题 A: 数据结构-队列-奇怪的电梯 我的代码 可以看成求一维平面的bfs最短路 #include <bits/stdc.h> using i64 long long; using pii std::pair<int,int>; co…...
算法刷题Day1
BM47 寻找第k大 第一天就随便记录吧,万事开头难,我好不容易开的头,就别难为自己,去追求高质量了。嘿嘿嘿 题目 传送门 解题思路一:维护一个大小为k的最小堆。最后返回堆顶元素。 代码: # # 代码中的类名…...
你还没有将 Siri 接入GPT对话功能吗?
由于各种原因,国内ios用户目前无缘自带 AI 功能,但是这并不代表国内 ios 无法接入 AI 功能,接下来手把手带你为iPhone siri 接入 gpt 对话功能。 siri 接入 chatGPT 暂时还无法下载 ChatGPT app,或者没有账号的读者可以直接跳到…...
LabVIEW 标准状态机设计模式
LabVIEW 标准状态机设计模式 LabVIEW 软件框架介绍LabVIEW编程模式及其应用分析状态机模式的类型分析标准状态机设计模式及状态机应用学习顺序结构它的一个缺点是什么? 状态机结构 LabVIEW 软件框架介绍 源于虚拟仪器技术的LabVIEW程序设计语言,从被创建…...
Scala学习记录,统计成绩
统计成绩练习 1.计算每个同学的总分和平均分 2.统计每个科目的平均分 3.列出总分前三名和单科前三名,并保存结果到文件中 解题思路如下: 1.读入txt文件,按行读入 2.处理数据 (1)计算每个同学的总分平均分 import s…...
使用 client-go 实现 Kubernetes 节点 Drain:详解与实战教程
在 Kubernetes 中使用 client-go 实现 drain 功能涉及多个步骤,需要模仿 kubectl drain 的行为。这包括将节点标记为不可调度(cordon)、驱逐 Pod,并处理 DaemonSet 和不可驱逐 Pod 的逻辑。以下是实现 drain 的主要步骤࿱…...
C#VB.Net项目一键多国语言显示
如何在项目什么都不做一键支持多国语言显示 开始我们的一键快捷使用之旅 01.创建多语言项目 02.一键批量窗口开启本地化,添加选中内容添加Mu方法 03.一键快捷翻译 04.运行查看效果 01.创建多语言项目 创建多语言项目前,请先下载安装,注册并登录. 为了便于演示这…...
【关闭or开启电脑自带的数字键盘】
目录 一、按数字键盘左上角的按键【NumLK Scroll】 二、修改注册表中数字键盘对应的数值【InitialKeyboardIndicators】 1、步骤: 2、知识点: 一、按数字键盘左上角的按键【NumLK Scroll】 这是最简单快捷的方法。 关闭后若想开启,再按一…...
如何在Bash中等待多个子进程完成,并且当其中任何一个子进程以非零退出状态结束时,使主进程也返回一个非零的退出码?
文章目录 问题回答参考 问题 如何在 Bash 脚本中等待该脚本启动的多个子进程完成,并且当这其中任意一个子进程以非零退出码结束时,让该脚本也返回一个非零的退出码? 简单的脚本: #!/bin/bash for i in seq 0 9; docalculations $i & d…...
Asio2网络库
header only,不依赖boost库,不需要单独编译,在工程的Include目录中添加asio2路径,在源码中#include <asio2/asio2.hpp>即可使用;支持tcp, udp, http, websocket, rpc, ssl, icmp, serial_port;支持可靠UDP(基于KCP),支持SSL;TCP支持各种数据拆包功能(单个字符或字符串或用…...
Uniapp 微信小程序内打开web网页
技术栈:Uniapp Vue3 简介 实际业务中有时候会需要在本微信小程序内打开web页面,这时候可以封装一个路由页面专门用于此场景。 在路由跳转的时候携带路由参数,拼接上web url,接收页面进行参数接收即可。 实现 webview页面 新…...
学习线性表_3
单链表的删除 直接删除即可删除后要free //删除第i个位置的元素 //删除时L是不会变的,所以不需要加引用 bool ListDelect(LinkList L,int i) {//i 1,即删除头指针//拿到要删除结点的前一个结点LinkList p GetElem(L,i-1);if(NULLp){return false;}//拿到要删除的结…...
智能桥梁安全运行监测系统守护桥梁安全卫士
一、方案背景 桥梁作为交通基础设施中不可或缺的重要组成部分,其安全稳定的运行直接关联到广大人民群众的生命财产安全以及整个社会的稳定与和谐。桥梁不仅是连接两地的通道,更是经济发展和社会进步的重要纽带。为了确保桥梁的安全运行,桥梁安…...
Arrays.asList()新增报错,该怎么解决
一、前言 在 Java 开发中,Arrays.asList() 是一个常用的工具方法,它允许开发者快速将数组转换为列表。尽管这个方法非常方便,但许多开发者在使用时可能会遭遇一个常见的错误:尝试向由 Arrays.asList() 返回的列表中添加元素时抛出…...
28.UE5实现对话系统
目录 1.对话结构的设计(重点) 2.NPC对话接口的实现 2.1创建类型为pawn的蓝图 2.2创建对话接口 3.对话组件的创建 4.对话的UI设计 4.1UI_对话内容 4.2UI_对话选项 4.3UI_对话选项框 5.对话组件的逻辑实现 通过组件蓝图,也就是下图中的…...
会议直击|美格智能亮相2024紫光展锐全球合作伙伴大会,融合5G+AI共拓全球市场
11月26日,2024紫光展锐全球合作伙伴大会在上海举办,作为紫光展锐年度盛会,吸引来自全球的众多合作伙伴和行业专家、学者共同参与。美格智能与紫光展锐竭诚合作多年,共同面向5G、AI和卫星通信为代表的前沿科技,聚焦技术…...
IDEA报错: java: JPS incremental annotation processing is disabled 解决
起因 换了个电脑打开了之前某个老项目IDEA启动springcloud其中某个服务直接报错,信息如下 java: JPS incremental annotation processing is disabled. Compilation results on partial recompilation may be inaccurate. Use build process “jps.track.ap.depen…...
面对深度伪造:OWASP发布专业应对指南
从美国大选造谣视频到AI编写的网络钓鱼邮件,深度伪造(deepfake)诈骗和生成式人工智能攻击日益猖獗,人眼越来越难以辨识,企业迫切需要为网络安全团队制定AI安全事件响应指南。 深度伪造攻击威胁日益增加 全球范围内&…...
IDEA全局设置-解决maven加载过慢的问题
一、IDEA全局设置 注意:如果不是全局设置,仅仅针对某个项目有效;例在利用网上教程解决maven加载过慢的问题时,按步骤设置却得不到解决,原因就是没有在全局设置。 1.如何进行全局设置 a.在项目页面,点击f…...
【阅读笔记】Android广播的处理流程
关于Android的解析,有很多优质内容,看了后记录一下阅读笔记,也是一种有意义的事情, 今天就看看“那个写代码的”这位大佬关于广播的梳理, https://blog.csdn.net/a572423926/category_11509429.html https://blog.c…...
queue 和 Stack
import scala.collection.mutable //queue:队列.排队打饭.... //特点:先进先出 //Stack:栈 //特点:先进后出 class ob5 { def main(args: Array[String]): Unit { val q1 mutable.Queue(1) q1.enqueue(2)//入队 q1.enqueue(3)//入队 q1.enqueue(4)…...
C#窗体小程序计算器
使其能完成2个数的加、减、乘、除基本运算。界面如下图,单击相应的运算符按钮,则完成相应的运算,并将结果显示出来,同时不允许在结果栏中输入内容 代码如下: private void button1_Click(object sender, EventArgs e)…...
Linux——自定义简单shell
shell 自定义shell目标普通命令和内建命令(补充) shell实现实现原理实现代码 自定义shell 目标 能处理普通命令能处理内建命令要能帮助我们理解内建命令/本地变量/环境变量这些概念理解shell的运行 普通命令和内建命令(补充) …...
大模型开发和微调工具Llama-Factory-->WebUI
WebUI LLaMA-Factory 支持通过 WebUI 零代码微调大模型。 通过如下指令进入 WebUI llamafactory-cli webui# 如果是国内, # USE_MODELSCOPE_HUB 设为 1,表示模型从 ModelScope 魔搭社区下载。 # 避免从 HuggingFace 下载模型导致网速不畅 USE_MODELSC…...
【网络】应用层协议HTTPHTTPcookie与sessionHTTPS协议原理
主页:醋溜马桶圈-CSDN博客 专栏:计算机网络原理_醋溜马桶圈的博客-CSDN博客 gitee:mnxcc (mnxcc) - Gitee.com 目录 1.应用层协议HTTP 2.认识 URL 2.1 urlencode 和 urldecode 3.HTTP 协议请求与响应格式 3.1 HTTP 请求 3.2 HTTP 响应 …...
基于LSTM的文本多分类任务
概述: LSTM(Long Short-Term Memory,长短时记忆)模型是一种特殊的循环神经网络(RNN)架构,由Hochreiter和Schmidhuber于1997年提出。LSTM被设计来解决标准RNN在处理序列数据时遇到的长期依赖问题…...
Git忽略文件
在Git中,你可以通过修改 .gitignore 文件来忽略整个文件夹。以下是具体步骤: 打开或创建 .gitignore 文件 确保你的项目根目录下有一个 .gitignore 文件。如果没有,创建一个: touch .gitignore 在 .gitignore 文件中添加要忽略…...
Spring的事务管理
tx标签用于配置事务管理用于声明和配置事务的相关属性 transaction-manager指定一个事务管理器的引用,用于管理事务的生命周期。propagation指定事务的传播属性,决定了在嵌套事务中如何处理事务。isolation指定事务的隔离级别,用于控制事务之…...
java int值可以直接赋值给char类型 详解
在 Java 中,int 值可以直接赋值给 char 类型,但有一定的限制和机制。以下是详细的解释: 1. Java 中的 char 和 int 类型关系 char 的本质 char 是一个 16 位无符号整数类型,用于表示 Unicode 字符。范围为 0 到 65535࿰…...
淘宝商品数据获取:Python爬虫技术的应用与实践
引言 随着电子商务的蓬勃发展,淘宝作为中国最大的电商平台之一,拥有海量的商品数据。这些数据对于市场分析、消费者行为研究、商品推荐系统等领域具有极高的价值。然而,如何高效、合法地从淘宝平台获取这些数据,成为了一个技术挑…...
【力扣】389.找不同
问题描述 思路解析 只有小写字母,这种设计参数小的,直接桶排序我最开始的想法是使用两个不同的数组,分别存入他们单个字符转换后的值,然后比较是否相同。也确实通过了 看了题解后,发现可以优化,首先因为t相…...
何时在 SQL 中使用 CHAR、VARCHAR 和 VARCHAR(MAX)
在管理数据库表时,考虑 CHAR、VARCHAR 和 VARCHAR(MAX) 是必不可少的。此外,使用正确的工具(例如dbForge Studio for SQL Server) ,与数据库相关的任务都会变得更加容易。它是针对 SQL Server 专业人员的强大的一体化解…...
pnpm安装electron出现postinstall$ node install.js报错
pnpm install --registryhttp://registry.npm.taobao.org安装依赖包的时候出现了postinstall$ node install.js报错 找到install.js 找到downloadArtifact方法,添加如下代码 mirrorOptions:{mirror:"http://npmmirror.com/mirrors/electron/"}http://n…...
如何从 Hugging Face 数据集中随机采样数据并保存为新的 Arrow 文件
如何从 Hugging Face 数据集中随机采样数据并保存为新的 Arrow 文件 在使用 Hugging Face 的数据集进行模型训练时,有时我们并不需要整个数据集,尤其是当数据集非常大时。为了节省存储空间和提高训练效率,我们可以从数据集中随机采样一部分数…...
Rook入门:打造云原生Ceph存储的全面学习路径(上)
文章目录 一.Rook简介二.Rook与Ceph架构2.1 Rook结构体系2.2 Rook包含组件2.3 Rook与kubernetes结合的架构图如下2.4 ceph特点2.5 ceph架构2.6 ceph组件 三.Rook部署Ceph集群3.1 部署条件3.2 获取rook最新版本3.3 rook资源文件目录结构3.4 部署Rook/CRD/Ceph集群3.5 查看rook部…...
AWS账号提额
Lightsail提额 控制台右上角,用户名点开,选择Service Quotas 在导航栏中AWS服务中找到lightsail点进去 在搜索框搜索instance找到相应的实例类型申请配额 4.根据自己的需求选择要提额的地区 5.根据需求来提升配额数量,提升小额配额等大约1小时生效 Ligh…...
计算机网络(三)
一个IP包,其数据长度为4900字节,通过一个MTU为1220字节的网络时,路由器的分片情况如何?请用图表的形式表示出路由器分片情况。 已知 IP 包的数据长度为 4900 字节,IP 首部长度通常为 20 字节,所以整个 IP …...
去哪儿Android面试题及参考答案
TCP 的三次握手与四次挥手过程是什么? TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议 ,三次握手和四次挥手是其建立连接和断开连接的重要过程。 三次握手过程 第一次握手:客户端向服务器发送一个 SYN(同步序列号)包,其中包…...
探索温度计的数字化设计:一个可视化温度数据的Web图表案例
随着科技的发展,数据可视化在各个领域中的应用越来越广泛。在温度监控和展示方面,传统的温度计已逐渐被数字化温度计所取代。本文将介绍一个使用Echarts库创建的温度计Web图表,该图表通过动态数据可视化展示了温度值,并通过渐变色…...
JS API事件监听(绑定)
事件监听 语法 元素对象.addEventListener(事件监听,要执行的函数) 事件监听三要素 事件源:那个dom元素被事件触发了,要获取dom元素 事件类型:用说明方式触发,比如鼠标单击click、鼠标经过mouseover等 事件调用的函数&#x…...
【k8s】kube-state-metrics 和 metrics-server
kube-state-metrics 和 metrics-server 是 Kubernetes 生态系统中两个重要的监控组件,但它们的功能和用途有所不同。下面是对这两个组件的详细介绍: kube-state-metrics 功能: kube-state-metrics 是一个简单的服务,它监听 Kub…...
Linux设置开启启动脚本
1.问题 每次启动虚拟机需要手动启动网络,不然没有enss33选项 需要启动 /mnt/hgfs/dft_shared/init_env/initaial_env.sh 文件 2.解决方案 2.1 修改/etc/rc.d/rc.local 文件 /etc/rc.d/rc.local 文件会在 Linux 系统各项服务都启动完毕之后再被运行。所以你想要…...
数据结构-图(一)
文章目录 图一、图的基本概念(一)图的定义(二)有向图与无向图(三)顶点的度、入度与出度(四)路径、回路与连通图 二、图的存储及基本操作(一)邻接矩阵…...
SQL面试题——抖音SQL面试题 最近一笔有效订单
最近一笔有效订单 题目背景如下,现有订单表order,包含订单ID,订单时间,下单用户,当前订单是否有效 +---------+----------------------+----------+-----------+ | ord_id | ord_time | user_id | is_valid | +---------+----------------------+--------…...