中国农村建设投资有限公司网站/山西网络推广
大家好,我是飞哥!
在过去的开发工作中,大家都是通过创建进程或者线程来工作的。Linux进程是如何创建出来的? 、聊聊Linux中线程和进程的联系与区别! 和你的新进程是如何被内核调度执行到的? 这几篇文章就是帮大家深入理解进程线程原理的。
但是,时至今日光了解进程和线程已经不够了。因为现在协程编程模型大行其道。很多同学知道进程和线程,但就是不理解协程是如何工作的。虽然能写出来代码,但不理解底层运行原理。
今天就让我以 golang 版本的 hello world 程序为例,给大家拆解一下协程编程模型的工作过程。
在本文中我会从 ELF 可执行文件的入口讲起,讲到 GMP 调度器的初始化,到主协程的创建,到主协程进入 runtime.main 最后执行到用户定义的 main 函数。
一、 hello world 程序的运行入口
golang 的 hello world 写起来非常的简单。
package main
import "fmt"
func main() {fmt.Println("Hello World!")
}
运行起来也是一样非常的简单。
# go build main.go
# ./main
Hello World!
程序是跑起来了,但是问题来了。传说中的协程究竟长什么样子,是何时被创建的,又是如何被加载运行并打印出 “Hello World!” 的。
不管是啥语言编译出来的可执行文件,都有一个执行入口点。shell 在将程序加载完后会跳转到程序入口点开始执行。
但值得提前说的一点是一般编程语言的入口点都不会是我们在代码中写的那个 main。c 语言中如此,golang 中更是这样。这是因为各个语言都需要在进程启动过程中做一些启动逻辑的。在 golang 中,其底层运行的 GMP、垃圾回收等机制都需要在进入用户的 main 函数之前启动起来。
接下来我们需要借助 readelf 和 nm 命令来找到上述编译出来的可执行文件 main 的执行入口。首先使用 readelf 找到 main 的入口点是在 0x45c220 位置处,如下图所示。
$ readelf --file-header main
ELF Header:......Entry point address: 0x45c220
那么 0x45c220 这个位置对应的是哪个函数呢?借助 nm 命令我们可以看到它是 _rt0_amd64_linux。
nm -n main | grep 45c220
000000000045c220 T _rt0_amd64_linux
这其实是一个汇编函数。
// file:asm_amd64.s
// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking.
TEXT _rt0_amd64(SB),NOSPLIT,$-8MOVQ 0(SP), DI // argcLEAQ 8(SP), SI // argvJMP runtime·rt0_go(SB)
这个函数的开头也有明确的注释 “_rt0_amd64 is common startup code for most amd64 systems when using internal linking”。这说明我们找对了。
接下来的 golang 运行就是顺着这个汇编函数开始执行,最后一步步地运行到我们所熟悉的 main 函数的。
二、入口执行分析
这一小节我们来看看 golang 程序在启动的时候都做了哪些事情。相信理解这些底层的工作机制对从事 golang 开发的同学会非常的大有裨益。
在上一小节我们看到了的 golang 入口函数 _rt0_amd64。要注意的是,当代码运行到这里的时候,操作系统已经为当前可执行文件创建好了一个主线程了。_rt0_amd64 只是将参数简单地保存一下后就 JMP (汇编中的函数调用)到 runtime·rt0_go 中了。
这个函数很长,我们只挑有重要的讲!
// file:runtime/asm_amd64.s
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0......// 2.1 Golang 核心初始化过程CALL runtime·osinit(SB)CALL runtime·schedinit(SB)//2.2 调用 runtime·newproc 创建一个协程// 并将 runtime.main 函数作为入口MOVQ $runtime·mainPC(SB), AX // entryPUSHQ AXCALL runtime·newproc(SB)POPQ AX//2.3 启动线程,启动调度系统CALL runtime·mstart(SB)
洋洋洒洒好几百行汇编代码,其实缩略完后,关键的核心逻辑就是上面几个关键点。
第一、通过 runtime 中的 osinit、schedinit 等函数对 golang 运行时进行关键的初始化。在这里我们将看到 GMP 的初始化,与调度逻辑。
第二、创建一个主协程,并指明 runtime.main 函数是其入口函数。因为操作系统加载的时候只创建好了主线程,协程这种东西还是得用户态的 golang 自己来管理。golang 在这里创建出了自己的第一个协程。
第三、调用 runtime·mstart 真正开启运行。
接下来我们分三个小节来详细了解下这三块的逻辑。
2.1 golang 核心初始化
golang 的核心初始化包括 runtime·osinit 和 runtime·schedinit 这两个函数。
在 runtime·osinit 中主要是获取CPU数量,页大小和 操作系统初始化工作。
// file:os_linux.go
func osinit() {ncpu = getproccount()physHugePageSize = getHugePageSize()osArchInit()
}
接下来是 runtime.schedinit 的初始化,这里主要是对调度系统的初始化。
在这个函数的注释中,也贴心地告诉了我们,golang 的 bootstrap(启动)流程步骤分别是 call osinit、call schedinit、make & queue new G 和 call runtime·mstart 四个步骤。这和我们前面说的一致。
Golang 中调度的核心就是 GMP 原理。这里我们不展开对 GMP 进行过多的说明,留着将来再细说。这里只提一下,在 runtime.schedinit 这个函数中,会将所有的 P 都给初始化好,并用一个 allp slice 维护管理起来。
// file:runtime/proc.go
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {......// 默认情况下 procs 等于 cpu 个数// 如果设置了 GOMAXPROCS 则以这个为准procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}// 分配 procs 个 Pif procresize(procs) != nil {throw("unknown runnable goroutine during bootstrap")}......
}
从上述源码中我们可以看到,P 的数量取决于当前 cpu 的数量,或者是 runtime.GOMAXPROCS 的配置。
不少 golang 的同学都有一种错误的认知,认为 runtime.GOMAXPROCS 限制的是 golang 中的线程数。这个认知是错误的。runtime.GOMAXPROCS 真正制约的是 GMP 中的 P,而不是 M。
再来简单看下 procresize,这个函数其实就是在维护 allp 变量,在这里保存着所有的 P。
// file:runtime/proc.go
// Change number of processors
// Returns list of Ps with local work, they need to be scheduled by the caller
func procresize(nprocs int32) *p {// 申请存储 P 的数组if nprocs > int32(len(allp)) {allp = ...}// 对新 P 进行内存分配和初始化,并保存到 allp 数组中for i := old; i < nprocs; i++ {pp := allp[i]if pp == nil {pp = new(p)}pp.init(i)atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))}...
}
2.2 golang 主协程的创建
汇编代码调用 runtime·newproc 创建一个协程,并将 runtime.main 函数作为入口。我们来看下第一个主协程是如何创建出来的。
//file:runtime/proc.go
func newproc(fn *funcval) {...systemstack(func() {newg := newproc1(fn, gp, pc)_p_ := getg().m.p.ptr()runqput(_p_, newg, true)if mainStarted {wakep()}})
}
systemstack 这个函数是 golang 内部经常使用的,runtime 代码经常通过调用 systemstack 临时性的切换到系统栈去执行一些特殊的任务。这里所谓的系统栈,就是操作系统视角创建出来的线程和线程栈。如果不理解,先不管这个也问题不大。
接着调用 newproc1 来创建一个协程出来,runqput 达标的是将协程添加到运行队列。最后的 wakep 是去唤醒一个线程去执行运行队列中的协程。
协程创建
我们一个一个分别来看。先看 newproc1 是如何创建协程的。
// file:runtime/proc.go
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {...//从缓存中获取或者创建 G 对象newg := gfget(_p_)if newg == nil {newg = malg(_StackMin)...}newg.sched.sp = spnewg.stktopsp = sp...newg.startpc = fn.fn...return newg
}
在 gfget 中是尝试从缓存中获取一个 G 对象出来。我们忽略这个逻辑,直接看 malg,因为它是创建一个 G。对我们理解更有帮助。在 malg 创建完后,对新的 gorutine 对象进行一些设置后就返回了。
在调用 malg 时传入了一个 _StackMin,这表示默认的栈大小,在 Golang 中的默认值是 2048。
这也就是很多人所说的 Golang 中协程很轻量,只需要消耗 2 KB 内存的缘由。但其实这个说法并不是很准确。首先这里分配的并不是 2KB,下面我们会看到还有有一些预留。另外当发生缺页中断的时候,Linux 是以 4 KB为单位分配的。
// file:runtime/proc.go
func malg(stacksize int32) *g {newg := new(g)if stacksize >= 0 {//这里会在 stacksize 的基础上为每个栈预留系统调用所需的内存大小 \_StackSystem//在 Linux/Darwin 上( \_StackSystem == 0 )本行不改变 stacksize 的大小stacksize = round2(_StackSystem + stacksize)}// 切换到 G0 为 newg 初始化栈内存systemstack(func() {newg.stack = stackalloc(uint32(stacksize))})// 设置 stackguard0 ,用来判断是否要进行栈扩容 newg.stackguard0 = newg.stack.lo + _StackGuardnewg.stackguard1 = ^uintptr(0)
}
在调用 malg 的时候会将传入的内存大小加上一个 _StackSystem 值预留给系统调用使用,round2 函数会将传入的值舍入为 2 的指数。然后会切换到 G0 执行 stackalloc 函数进行栈内存分配。分配完毕之后会设置 stackguard0 为 stack.lo + _StackGuard,作为将来判断是否需要进行栈扩容使用。
//file:runtime/stack.go
func stackalloc(n uint32) stack {thisg := getg()...//对齐到整数页n = uint32(alignUp(uintptr(n), physPageSize))v := sysAlloc(uintptr(n), &memstats.stacks_sys)return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
其中栈是这样一个结构体
//file:runtime/runtime2.go
type stack struct {lo uintptrhi uintptr
}
sysAlloc 使用 mmap 系统调用来真正为协程栈申请指定大小的地址空间。
// file:runtime/mem_darwin.go
func sysAlloc(n uintptr, sysStat *sysMemStat) unsafe.Pointer {v, err := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0)if err != 0 {return nil}sysStat.add(int64(n))return v
}
将协程添加到运行队列
在协程创建出来后,会调用 runqput 将它添加到运行队列中。在讲这块的逻辑之前,我们得首先讲讲 Golang 中的运行队列。
Golang 为什么会抽象出一个 P 来呢。这是因为 Golang 在 1.0 版本的多线程调度器的实现中,调度器和锁都是全局资源,锁的竞争和开销非常的大,导致性能比较差。
其实这个问题在 Linux 中早已很好地解决了。Golang 就把它学来了。在 Linux 中每个 CPU 核都有一个 runqueue,来保存着将来要在该核上调度运行的进程或线程。这样调度的时候,只需要看当前的 CPU 上的资源就行,把锁的开销就砍掉了。
所以,Golang 中的 P 可以认为是对 Linux 中 CPU 的一个虚拟,目的是和 Linux 一样,找一个无竞争地保管运行队列资源的方法。在 Golang 中,每个 P 都有它的运行队列。
理解了这个背景,我们再来看 Golang 中的 runqput 是如何将协程添加到 P 的运行队列中的。
// file:runtime/proc.go
// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the _p_.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
func runqput(_p_ *p, gp *g, next bool) {...//将新 goroutine 添加到 P 的 runnext 中if next {retryNext:oldnext := _p_.runnextif !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {goto retryNext}if oldnext == 0 {return}// 将原来的 runnext 添加到运行队列中gp = oldnext.ptr()}//将新协程或者被从 runnext 上踢下来的协程添加到运行队列中
retry:h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumerst := _p_.runqtail//如果 P 的运行队列没满,那就添加到尾部if t-h < uint32(len(_p_.runq)) {_p_.runq[t%uint32(len(_p_.runq))].set(gp)atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumptionreturn}//如果满了,就添加到全局运行队列中if runqputslow(_p_, gp, h, t) {return}
}
在 runqput 中首先尝试将新协程放到 runnext 中,这个有优先执行权。然后会将新协程,或者被新协程从 runnext 上踢下来的协程加入到当前 P(运行队列)的尾部去。但还有可能当前这个运行队列已经任务过多了,那就需要调用 runqputslow 分一部分运行队列中的协程到全局队列中去。以便于减轻当前运行队列的执行压力。
唤醒一个线程去
前面只是将新创建的 goroutine 添加到了 P 的运行队列中。现在 GMP 中的 G 有了,P 也有了,就还差 M 了。真正的运行还是需要操作系统的线程去执行的。
// file:runtime/proc.go
func wakep() {...startm(nil, true)
}
wakep 核心是调用 startm。这个函数将会调度线程去运行 P 中的运行队列。如果有必要的话,可能也需要创建新线程出来。
// file:runtime/proc.go
// Schedules some M to run the p (creates an M if necessary).
func startm(_p_ *p, spinning bool) {mp := acquirem()//如果没有传入 p,就获取一个 idel pif _p_ == nil {_p_ = pidleget()}//再获取一个空闲的 mnmp := mget()if nmp == nil {//如果获取不到,就创建一个出来newm(fn, _p_, id)...return}...
}
2.3 启动调度系统
现在 GMP 中的三元素全具备了,而且主协程中的运行函数 fn 也指定为了 runtime.main。接下来就是调用 mstart 来启动线程,启动调度系统。
汇编中的 mstart 函数调用的是 golang 源码中的 mstart0
// file:runtime/proc.go
func mstart0() {...mstart1()
}
// file:runtime/proc.go
func mstart1() {...// 进入调度循环schedule()
}
其中,schedule 是整个 golang 程序的运行核心。所有的协程都是通过它来开始运行的。
schedule 的主要工作逻辑有这么几点
每隔 61 次调度轮回从全局队列找,避免全局队列中的g被饿死。
从 p.runnext 获取 g,从 p 的本地队列中获取。
调用 findrunnable 找 g,找不到的话就将 m 休眠,等待唤醒。
当找到一个 g 后,就会调用 execute 去执行 g
然后再来看源码就很容易理解了。
// file:runtime/proc.go
func schedule() {_g_ := getg()...
top:pp := _g_.m.p.ptr()//每 61 次从全局运行队列中获取可运行的协程if 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 {//从当前 P 的运行队列中获取可运行gp, inheritTime = runqget(_g_.m.p.ptr())}if gp == nil {//当前P或者全局队列中获取可运行协程//尝试从其它P中steal任务来处理//如果获取不到,就阻塞gp, inheritTime = findrunnable() // blocks until work is available}//执行协程execute(gp, inheritTime)
}
其中 findrunnable 如果从当前 P 的运行队列和全局运行队列获取 G 都没有任务后,还会尝试从其它的 P 中获取一些任务过来运行。代码就不过多展示了。
三、main 函数的真正运行
至此,整个 golang 的调度系统就算是跑起来了。因为前面我们创建了主协程,而且还给它设置了 runtime.main 函数作为入口。所以对于主协程的调度,就会进入这个入口进行执行。终于,能看到 runtime 快运行到我们自己写的 main 函数中了。
runtime.main 在执行 main 包中的 main 之前,还是做了一些不少其他工作,包括:
新建一个线程来执行 sysmon。sysmon的工作是系统后台监控(定期垃圾回收和调度抢占)。
执行 runtime init 函数。runtime 包中也有不少的 init 函数,会在这个时机运行
启动 gc 清扫的 goroutine。
执行 main init 函数。包括用户定义的所有的 init 函数。
执行用户 main 函数。
// file:runtime/proc.go
// The main goroutine.
func main() {g := getg()// 在系统栈上运行 sysmonsystemstack(func() {newm(sysmon, nil, -1)})// runtime 内部 init 函数的执行,编译器动态生成的。doInit(&runtime_inittask) // Must be before defer.// gc 启动一个goroutine进行gc清扫gcenable()// 执行main initdoInit(&main_inittask)// 执行用户mainfn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtimefn()// 退出程序exit(0)
}
好了,终于,我们定义的 main 函数能被执行到。可以输出 “Hello World!”了。
四、总结
Golang 程序的运行入口是 runtime 定义的一个汇编函数。这个函数核心有三个逻辑:
第一、通过 runtime 中的 osinit、schedinit 等函数对 golang 运行时进行关键的初始化。在这里我们将看到 GMP 的初始化,与调度逻辑。
第二、创建一个主协程,并指明 runtime.main 函数是其入口函数。因为操作系统加载的时候只创建好了主线程,协程这种东西还是得用户态的 golang 自己来管理。golang 在这里创建出了自己的第一个协程。
第三、调用 runtime·mstart 真正开启调度器进行运行。
当调度器开始执行后,其中主协程会进入 runtime.main 函数中运行。在这个函数中进行几件初始化后,最后后真正进入用户的 main 中运行。
第一、新建一个线程来执行 sysmon。sysmon的工作是系统后台监控(定期垃圾回收和调度抢占)。
第二、启动 gc 清扫的 goroutine。
第三、执行 runtime init,用户 init。
第四、执行用户 main 函数。
看似简简单单的一个 Golang 的 Hello World 程序,只要你愿意深挖,里面真的有极其丰富的营养的!
如果觉得有用,期待和给你的朋友一起分享~
相关文章:

深入剖析 Golang 程序启动原理 - 从 ELF 入口点到GMP初始化到执行 main!
大家好,我是飞哥! 在过去的开发工作中,大家都是通过创建进程或者线程来工作的。Linux进程是如何创建出来的? 、聊聊Linux中线程和进程的联系与区别! 和你的新进程是如何被内核调度执行到的? 这几篇文章就是…...

C语言——多文件编程
多文件编程 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件在头文件对应的xxx.c中实现xxx.h声明的函数 防止头文件重复包含 当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头…...

Git学习part1
02.尚硅谷_Git&GitHub_为什么要使用版本控制_哔哩哔哩_bilibili 1.Git必要性 记录代码开发的历史状态 ,允许很多人同时修改文件(分布式)且不会丢失记录 2.版本控制工具应该具备的功能 1)协同修改 多人并行不悖的修改服务器端…...

2309C++均为某个类型
#include <常用> 构 A{空 f(){打印("啊");} }; 元<类 T>构 是点啊:假型{}; 元<>构 是点啊<A>:真型{}; 元<类 T>概念 是呀是点啊<T>::值;元<是呀...T>空 f(T&...t){(t.f(),...); }//均为元<类...T>要求 均为值&l…...

2023年打脸面试官之TCP--瞬间就懂
1.TCP 三次握手之为什么要三次呢?事不过三? 过程如下图: 先来解释下上述的各个标志的含义 序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节…...

设计模式-单例模式Singleton
单例模式 单例模式 (Singleton) (重点)1) 为什么要使用单例2) 如何实现一个单例2.a) 饿汉式2.b) 懒汉式2.c) 双重检查锁2.d) 静态内部类2.e) 枚举类2.f) 反射入侵2.g) 序列化与反序列化安全 3) 单例存在的问题3.a) 无法支持面向对象编程 单例模式 (Singleton) (重点) 一个类只…...

PPPoE连接无法建立的排查和修复
嗨,亲爱的读者朋友们!你是否曾经遇到过PPPoE连接无法建立的问题?今天我将为你详细解析排查和修复这个问题的步骤。 检查物理连接 首先,我们需要确保物理连接没有问题。请按照以下步骤进行检查: - 检查网线是否插好&…...

QT 发布软件基本操作
一、配置环境变量 找到Qt安装时的bin目录的路径:D:\Qt\Qt5.14.2\5.14.2\mingw73_64\bin,将目录拷贝至下述环境变量中。 打开计算机的高级系统设置 选中环境变量-->系统变量-->Path 点击编辑-->新建-->粘贴 二、生成发布软件的可执行程序 …...

CTFhub-SSRF-内网访问
CTFHub 环境实例 | 提示信息 http://challenge-8bf41c5c86a8c5f4.sandbox.ctfhub.com:10800/?url_ 根据提示,在url 后门添加 127.0.0.1/flag.php http://challenge-8bf41c5c86a8c5f4.sandbox.ctfhub.com:10800/?url127.0.0.1/flag.php ctfhub{a6bb51530c8f6be0…...

Cenos7安装小火车程序动画
运维Shell脚本小试牛刀(一) 运维Shell脚本小试牛刀(二) 运维Shell脚本小试牛刀(三)::$(cd $(dirname $0); pwd)命令详解 运维Shell脚本小试牛刀(四): 多层嵌套if...elif...elif....else fi_蜗牛杨哥的博客-CSDN博客 Cenos7安装小火车程序动画 一:替换…...

Node 执行命令时传参 process.argv
process 对象是一个全局变量,提供当前 Node.js 进程的有关信息,以及控制当前 Node.js 进程。 因为是全局变量,所以无需使用 require()。 process.argv 属性返回一个数组,这个数组包含了启动Node.js进程时的命令行参数,…...

【Vue】快速上手--Vue 3.0
什么是 Vue? Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的…...

PyTorch深度学习遥感影像地物分类与目标检测、分割及遥感影像问题深度学习优化实践技术应用
我国高分辨率对地观测系统重大专项已全面启动,高空间、高光谱、高时间分辨率和宽地面覆盖于一体的全球天空地一体化立体对地观测网逐步形成,将成为保障国家安全的基础性和战略性资源。未来10年全球每天获取的观测数据将超过10PB,遥感大数据时…...

04、添加 com.fasterxml.jackson.dataformat -- jackson-dataformat-xml 依赖报错
Correct the classpath of your application so that it contains a single, compatible version of com.fasterxml.jackson.dataformat.xml.XmlMapper 解决: 改用其他版本,我没写版本号,springboot自己默认的是 2.11.4 版本 成功启动项目…...

禅道项目管理系统 - 操作使用 (2023版)
1. 部门-用户-权限 新增部门 新增用户 设置权限 2. 项目集创建 项目集 - 添加项目集 3. 产品线创建 产品 - 产品线 4. 产品创建 产品 - 产品列表 - 添加产品 5. 产品计划创建 产品 - xx产品 - 计划 - 创建计划 我这里创建3个计划 (一期, 二期, 三期) 6. 研发需求 - 创建模块…...

C++的多重继承
派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。 多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。 …...

ZooKeeper与Paxos
Apache ZooKeeper是由Apache Hadoop的子项目发展而来,于2010年11月正式成为了Apache的顶级项目。ZooKeeper为分布式应用提供了高效且可靠的分布式协调服务,提供了诸如统一命名服务、配置管理和分布式锁等分布式的基础服务。在解决分布式数据一致性方面&a…...

Cargo 静态编译
git clone --recursive https://github.com/kornelski/pngquant.git vi ~/.cargo/config.toml[http] debug true proxy "127.0.0.1:1080" 1.apt 更新 2.apt install cargo 3.修改源码的Cargo.toml [source.crates-io] #registry "https://code.aliyun.com…...

【多线程】有两个线程都能访问n,初始时n为0,⼀个线程执⾏n++,n+=2,另⼀个线程执⾏n+=3,当两个线程都执行完后n可能的值
必备知识点:n 在底层是由三条指令在CPU完成的 load : 将内存的值读取到CPU寄存器add : 将CPU寄存器中的值进行1操作save : 将CPU寄存器中的值写回内容 回答 首先n操作在底层是由三条指令在CPU完成的,先要将内存中n的值读取到CPU寄存器,然后…...

Jtti:如何通过宝塔面板快速安装WordPress博客源码?
通过宝塔面板快速安装WordPress博客源码是非常简单的。宝塔面板提供了图形化界面,使安装过程变得直观和方便。以下是通过宝塔面板安装WordPress的步骤: 登录宝塔面板: 打开您的Web浏览器,访问您的宝塔面板地址(通常是 …...

Windows右键添加用 VSCODE 打开
1.安装VSCODE时 安装时会有个选项来添加,如下: ①将“通过code 打开“操作添加到windows资源管理器文件上下文菜单 ②将“通过code 打开”操作添加到windows资源管理器目录上下文菜单 说明:①②勾选上,可以对文件,目…...

达梦数据库管理用户和创建用户介绍
概述 本文主要对达梦数据库管理用户和创建用户进行介绍和总结。 1.管理用户介绍 1.1 达梦安全机制 任何数据库设计和使用都需要考虑安全机制,达梦数据库采用“三权分立”或“四权分立”的安全机制,将系统中所有的权限按照类型进行划分,为每…...

使用python,生成数字在图片上的验证码
许多网站在注册时都要求输入验证码,这样做为了防止被程序恶意注册和保证网站安全 1. Pillow PIL(Python Imaging Library)是一个强大的python图像处理库,只是支持到python2.7, Pillow虽说是PIL的一个分支,但是pillow支持python3.xÿ…...

阿晨的运维笔记 | CentOS部署Docker
使用yum安装 # step 1: 安装必要的一些系统工具 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 # Step 2: 添加软件源信息 sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # Step 3: 更新并安装 …...

自动化运维:Ansible基础与命令行模块操作
目录 一、理论 1. Ansible 2.部署Ansible自动化运维工具 3.Ansible常用模块 4.hostsinverntory主机清单 二、实验 1.部署Ansible自动化运维工具 2.ansible 命令行模块 3.hostsinverntory主机清单 三、问题 1. ansible远程shell失败 2.组变量查看webservers内主机ip报…...

深度学习6:自然语言处理-Natural language processing | NLP
目录 NLP 为什么重要? 什么是自然语言处理 – NLP NLP 的2大核心任务 自然语言理解 – NLU|NLI 自然语言生成 – NLG NLP(自然语言处理) 的5个难点 NLP 的4个典型应用 NLP 的 2 种途径、3 个核心步骤 总结 自然语言处理 NLP 为什么重要? “语言…...

Mysql多表操作
文章目录 1. 概述2. 内连接3. 外连接4. 自连接5. 联合查询-union,union all6. 子查询 1. 概述 在项目开发中,在进行数据库表结构设计是,会根据业务需求和业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所…...

【leetcode 力扣刷题】数学题之计算次幂//次方:快速幂
利用乘法求解次幂问题—快速幂 50. Pow(x, n)372. 超级次方 50. Pow(x, n) 题目链接:50. Pow(x, n) 题目内容: 题目就是要求我们去实现计算x的n次方的功能函数,类似c的power()函数。但是我们不能使用power()函数直接得到答案,那…...

【核心复现】基于改进灰狼算法的并网交流微电网经济优化调度(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

Cannal监听binlog
文章目录 一、canal概念二、canal使用场景四、Canal工作原理Mysql主从复制原理 binlog中的二进制日志binlog格式选择 Canal消费方式应用实践总结 一、canal概念 canal是用java开发的基于数据库增量日志解析,提供增量数据订阅&消费的中间件。目前,ca…...