golang并发编程-channel

在golang 并发编程里,经常会听到一句话:不要通过共享内存进行通信,通过通信来共享内存。下面我们会介绍下channel, 通过源码的方式去了解channel是怎么工作的。

基本结构

流程图

代码解读

type hchan struct {qcount   uint           // 队列中的总数据dataqsiz uint           // 环形队列的大小buf      unsafe.Pointer // 指向数据数组 qsiz 的元素elemsize uint16 // 循环队列中元素的大小;closed   uint32 // chan关闭标志elemtype *_type // 循环队列中元素的类型sendx    uint   // 待发送的数据在循环队列buf中的索引recvx    uint   // 待接收的数据在循环队列buf中的索引recvq    waitq  // 等待发送数据的 goroutinesendq    waitq  // 等待接收数据的 goroutine// 锁保护hchan中的所有字段,以及此chan 上被阻止的sudogs中的几个字段。lock mutex
}
type waitq struct {first *sudoglast  *sudog
}
// sudog代表等待列表中的g,例如用于在通道上进行发送/接收。
type sudog struct {g *g next *sudogprev *sudogelem unsafe.Pointer // 数据元素 (可能指向栈)// 以下字段永远不会同时访问。// 对于chan,waitlink仅由g访问。// 对于信号量,所有字段(包括上面的那些)只有在持有semaRoot锁时才能访问。acquiretime int64releasetime int64ticket      uint32// isSelect表示g在 select 上isSelect bool// success 表示通道 c 上的通信是否成功。// 如果 goroutine 被唤醒是因为通道 c 上传递了值,则为 true,// 如果唤醒是因为通道 c 关闭,则为 false。success boolparent   *sudog // semaRoot binary treewaitlink *sudog // g.waiting list or semaRootwaittail *sudog // semaRootc        *hchan // channel
}

创建

例子

// make(chan 类型, 大小)
chBuf := make(chan int, 1024) // 缓冲队列
ch := make(chan int) // 无缓存队列(也叫同步队列)

我很重要: channel带缓存的异步,不带缓存同步

思维图

代码解读

func makechan(t *chantype, size int) *hchan {elem := t.elem// 检查数据是不是超过64kbif elem.size >= 1<<16 {throw("makechan: invalid channel element type")}if hchanSize%maxAlign != 0 || elem.align > maxAlign {throw("makechan: bad alignment")}// 检查缓存是否溢出mem, overflow := math.MulUintptr(elem.size, uintptr(size))if overflow || mem > maxAlloc-hchanSize || size < 0 {panic(plainError("makechan: size out of range"))}// 当buf中存储的元素不包含指针时,chan不包含对GC感兴趣的指针。// buf指向相同的分配,elemtype固定不变。// SudoG 的引用来自其所属的线程,因此无法收集。// TODO(dvyukov,rlh):重新思考收集器何时可以移动分配的对象。var c *hchanswitch {case mem == 0:  // 队列或元素大小为零// 只为hchan分配内存c = (*hchan)(mallocgc(hchanSize, nil, true))// 竞态检查,利用这个地址进行同步操作.  c.buf = c.raceaddr()case elem.ptrdata == 0://元素不是指针// 分配一块连续的内存给hchan和buf    c = (*hchan)(mallocgc(hchanSize+mem, nil, true))c.buf = add(unsafe.Pointer(c), hchanSize)default: // 默认// 单独为 hchan 和缓冲区分配内存c = new(hchan)c.buf = mallocgc(mem, elem, true) }c.elemsize = uint16(elem.size) // 循环队列中元素的大小c.elemtype = elem // 元素类型c.dataqsiz = uint(size)  lockInit(&c.lock, lockRankHchan) // 锁优先级if debugChan {print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")}return c
}

channel 创建的时候分以下三种情况:

1- 队列或元素大小为零,调用mallocgc 在堆上给chan 创建一个hchansize大小的内存空间

2- 元素不是指针,调用mallocgc 在堆上给chan 创建一个 大小hchansize+ mem连续的内存空间(队列+buf缓存空间)

3- 默认情况下,调用mallocgc 单独为 hchan 分配内存

发送

代码解读

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {if c == nil { // 如果chan为nilif !block { // 非阻塞直接返回flasereturn false}// 阻塞直接挂起抛出异常gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)throw("unreachable")}if debugChan {print("chansend: chan=", c, "\n")}if raceenabled {racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))}// Fast path:在不获取锁的情况下检查失败的非阻塞操作。// 在观察到通道未关闭后,我们观察到 channel 未准备好发送。(c.closed和full())//当 channel 不为 nil,并且 channel 没有关闭时,// 如果没有缓冲区且没有接收者rec或者缓冲区已经满了,返回 false。if !block && c.closed == 0 && full(c) {return false}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}lock(&c.lock) // 加锁if c.closed != 0 { // 如果已经关闭了,就抛出异常unlock(&c.lock)panic(plainError("send on closed channel"))}// 有等待的接收者。if sg := c.recvq.dequeue(); sg != nil {// 我们将要发送的值直接传递给send,绕过通道缓冲区(如果有的话)。send(c, sg, ep, func() { unlock(&c.lock) }, 3)return true}// 缓冲区存在空余空间时if c.qcount < c.dataqsiz {qp := chanbuf(c, c.sendx)     // 找到要发送数据到循环队列buf的索引位置if raceenabled {racenotify(c, c.sendx, nil)}// 数据拷贝到循环队列中typedmemmove(c.elemtype, qp, ep)// 将待发送数据索引加1,c.sendx++// 如果到了末尾,从0开始 (循环队列)if c.sendx == c.dataqsiz {c.sendx = 0}// chan中元素个数加1c.qcount++// 释放锁返回trueunlock(&c.lock)return true}if !block { // 缓冲区没有空间,直接返回falseunlock(&c.lock)return false}// channel上阻塞。一些receiver将为我们完成操作。gp := getg() // 获取发送数据使用的 G// acquireSudog() 获取一个 sudog,如果缓存没有获取的到,就新建一个。mysg := acquireSudog() mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}// 在分配 elem 和将 mysg 加入 gp.waiting 队列,// 没有发生堆栈分裂,copystack 可以找到它。mysg.elem = epmysg.waitlink = nil mysg.g = gpmysg.isSelect = false // 是否在 select mysg.c = cgp.waiting = mysggp.param = nil// 调用 c.sendq.enqueue 方法将配置好的 sudog 加入待发送的等待队列c.sendq.enqueue(mysg)atomic.Store8(&gp.parkingOnChan, 1)// 调用gopark方法挂起当前goroutine,状态为waitReasonChanSend,阻塞等待channel接收者的激活gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)// 确保发送的值保持活动状态,直到接收者将其复制出来KeepAlive(ep)// G 被唤醒if mysg != gp.waiting { // 如果sudog 结构体中等待的 g  和获取到不一致throw("G waiting list is corrupted")}gp.waiting = nil。// 等待请空gp.activeStackChans = falseclosed := !mysg.successgp.param = nilif mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}mysg.c = nilreleaseSudog(mysg)if closed {if c.closed == 0 {throw("chansend: spurious wakeup")}panic(plainError("send on closed channel"))}return true
}
  1. chan 为nil 非阻塞的话就返回false, 阻塞panic
  2. 当 channel 不为 nil且 channel 没有关闭时,如果没有缓冲区且没有接收者rec或者缓冲区已经满了,返回 false。
  3. 如果存在等待的接收者,通过 send 直接将数据发送给阻塞的接收者
  4. 如果缓冲区存没有满,chanbuf + typedmemmove 将发送的数据写入 chan 的缓冲区;
  5. 如果缓冲区已满时,把发送数据的goroutin sendq中等待其他 G 接收数据;
func full(c *hchan) bool {// c.dataqsiz是不可变的(在创建通道后不可再写操作),因此在通道操作期间的任何时候读取都是安全的。if c.dataqsiz == 0 { // 数据是空的return c.recvq.first == nil//指针读取是近似原子性的}// 数据满了return c.qcount == c.dataqsiz//指针读取是近似原子性的
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if raceenabled {if c.dataqsiz == 0 {racesync(c, sg)} else {// 如果我们通过缓冲区,即使我们直接复制。//请注意,我们只需要在raceenabled时增加头/尾位置。racenotify(c, c.recvx, nil)racenotify(c, c.recvx, sg)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0}c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz -- 发送的索引位置获取原理}}if sg.elem != nil {// 直接把要发送的数据拷贝到receiver的内存地址sendDirect(c.elemtype, sg, ep)sg.elem = nil}gp := sg.gunlockf()gp.param = unsafe.Pointer(sg)sg.success = trueif sg.releasetime != 0 {sg.releasetime = cputicks()}// 将等待接收数据的 G 标记成可运行状态 Grunnable (goready-ready: casgstatus(gp, _Gwaiting, _Grunnable))//把该 G 放到发送方所在的处理器的 runnext 上等待执行, 该处理器在下一次调度时会立刻唤醒数据的接收方(runqput方法)// runqput 尝试将 g 放入本地可运行队列:// 如果 next 为 false,runqput 会将 g 添加到可运行队列的尾部。// 如果 next 为 true,runqput 将 g 放入 _p_.runnext 槽中。 这里为true// 如果运行队列已满,runnext 会将 g 放入全局队列。// 仅由所有者 P 执行。goready(gp, skip+1) // 唤醒等待的接收者goroutine
}

1- 直接把要发送的数据拷贝到receiver的内存地址

2- 将等待接收数据的 G 标记成可运行状态 Grunnable 。把该 G 放到发送方所在的处理器的 runnext 上等待执行, 该处理器在下一次调度时会立刻唤醒数据的接收方

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {//src 在我们的栈上,dst 是另一个栈上的一个插槽。// 一旦我们从sg中读取了sg.elem,如果目标堆栈被复制(收缩),它将不再被更新。// 因此,请确保在读取和使用之间不会发生抢占点。dst := sg.elemtypeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)// 不需要cgo写屏障检查,因为dst始终是Go内存。memmove(dst, src, t.size)
}

sendDirect:数据拷贝到了接收者的内存地址上

// chanbuf(c, i) 是指向缓冲区中第 i 个槽的指针
func chanbuf(c *hchan, i uint) unsafe.Pointer {return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}

接收

代码解读
 

//chanrecv在通道c上接收数据,并将接收到的数据写入ep。
//ep可能为零,在这种情况下,收到的数据将被忽略。
//如果 block == false 且没有可用元素,则返回 (false, false)。
//否则,如果c是闭的,则返回零*ep并返回(true,false)。
//否则,用元素填充 *ep 并返回 (true, true)。
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {// raceenabled:不需要检查ep,因为它始终位于堆栈上,或者是由reflect分配的新内存if debugChan {print("chanrecv: chan=", c, "\n")}if c == nil {if !block { // 如果c为空且是非阻塞调用,那么直接返回 (false, false)return}// 若果阻塞直接挂起,抛出错误gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)throw("unreachable")}// Fast path: 在不获取锁的情况下检查失败的非阻塞操作if !block && empty(c) { // 通过empty()判断是无缓冲chan或者是chan中没有数据// 在观察到channel未准备好接收后,看channel是否已关闭。if atomic.Load(&c.closed) == 0 { // 如果chan没有关闭,则直接返回 (false, false)return}// 如果chan关闭, 为了防止检查期间的状态变化,二次调用empty()进行原子检查二次调用empty()进行原子检查,// 如果是无缓冲chan或者是chan中没有数据,返回 (true, false) // 通道已不可逆地关闭。 为了防止检查期间的状态变化,重新检查通道是否有任何待接收的数据if empty(c) {if raceenabled {raceacquire(c.raceaddr())}if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}lock(&c.lock)// 如果已经关闭且chan中没有数据,返回 (true,false)if c.closed != 0 && c.qcount == 0 {if raceenabled {raceacquire(c.raceaddr())}unlock(&c.lock)if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}// 从sendq 队列获取等待发送的 G     if sg := c.sendq.dequeue(); sg != nil {// 在 channel 的sendq队列中找到了等待发送的 G,取出队头等待的G。// 找到一个等待发送的sender 。如果缓冲区大小为 0,则直接从sender接收值 。// 否则,从队列头部接收,并将发送者的值添加到队列尾部(由于队列已满,这两个值都映射到同一个缓冲区下标)。recv(c, sg, ep, func() { unlock(&c.lock) }, 3)return true, true}// 如果缓冲区有数据if c.qcount > 0 {// 直接从队列接收qp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)}// 接收数据地址ep不为空,直接从缓冲区复制数据到ep  if ep != nil {typedmemmove(c.elemtype, ep, qp)}typedmemclr(c.elemtype, qp)c.recvx++ // 待接收索引加1if c.recvx == c.dataqsiz { // 如果循环队列到了末尾,从0开始 c.recvx = 0}c.qcount-- // 缓冲区数据减1unlock(&c.lock)return true, true}if !block { // 如果是select非阻塞读取的情况,直接返回(false, false)unlock(&c.lock)return false, false}//没有可用的发送者:阻塞channel。gp := getg() // 获取当前 goroutine 的指针,用于绑定给一个 sudog mysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}// No stack splits between assigning elem and enqueuing mysg// on gp.waiting where copystack can find it.mysg.elem = epmysg.waitlink = nilgp.waiting = mysgmysg.g = gpmysg.isSelect = falsemysg.c = cgp.param = nilc.recvq.enqueue(mysg)// Signal to anyone trying to shrink our stack that we're about// to park on a channel. The window between when this G's status// changes and when we set gp.activeStackChans is not safe for// stack shrinking.atomic.Store8(&gp.parkingOnChan, 1)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)// someone woke us upif mysg != gp.waiting {throw("G waiting list is corrupted")}gp.waiting = nilgp.activeStackChans = falseif mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}success := mysg.successgp.param = nilmysg.c = nilreleaseSudog(mysg)return true, success
}
  1. 如果chan 为nil ,非阻塞的话返回(false, false),阻塞的panic
  2. chan已经关闭并且缓存没有数据,直接返回(false, false)
  3. 如果是无缓冲chan或者是chan中没有数据,返回 (true, false)
  4. chan 的sendq队列中存在挂起的G, 会将recvx 索引的数据拷贝到接收变量内存中并将sendq队列中G 数据拷到缓存
  5. 如果缓冲区有数据,直接读取recvx索引的数据
  6. 如果没有可用的发送者,获取当前 G 的指针,用于绑定给一个 sudog 并加入到recvq,等待调度器唤醒
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if c.dataqsiz == 0 {if raceenabled {racesync(c, sg)}if ep != nil {// 从sender 复制数据recvDirect(c.elemtype, sg, ep)}} else {// 队列已满。从队列的头部取走该项目。// 让发送方将其项目排入队列的尾部。// 由于队列已满,这两个位置是相同的。qp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)racenotify(c, c.recvx, sg)}// 将数据从队列复制到receiverif ep != nil {typedmemmove(c.elemtype, ep, qp)}// 将数据从sender复制到队列typedmemmove(c.elemtype, qp, sg.elem)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0}c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz}sg.elem = nilgp := sg.gunlockf()gp.param = unsafe.Pointer(sg)sg.success = trueif sg.releasetime != 0 {sg.releasetime = cputicks()}goready(gp, skip+1)
}
  • 如果 chan 不存在缓冲区:调用 recvDirect 将 发送队列中 G 存储的 elem 数据拷贝到目标内存地址中;
  • 如果 chan 存在缓冲区:
    • 1- 将队列中的数据拷贝到receiver 的内存地址;
    • 2- 将 sender 头的数据拷贝到缓冲区中,释放一个阻塞的 sender;
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {// dst 在我们的栈或堆上,src 在另一个栈上。// chan已锁定,因此在此操作期间src将不会移动。src := sg.elemtypeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)memmove(dst, src, t.size)
}
// empty报告从c读取是否会阻塞(即通道为空)。它使用对可变状态的单个原子读取。
func empty(c *hchan) bool {  // c.dataqsiz 是不可变的  if c.dataqsiz == 0 {    // 发送队列为空return atomic.Loadp(unsafe.Pointer(&c.sendq.first)) == nil }  return atomic.Loaduint(&c.qcount) == 0
}

关闭

代码解读
 

func closechan(c *hchan) {if c == nil { // 关闭空的chan,会panicpanic(plainError("close of nil channel"))}lock(&c.lock)if c.closed != 0 { // 关闭已经关闭的chan,会panicunlock(&c.lock)panic(plainError("close of closed channel"))}if raceenabled {callerpc := getcallerpc()racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))racerelease(c.raceaddr())}// chan的closed置为关闭状态c.closed = 1var glist gList // 申明一个存放所有接收者和发送者goroutine的list// 处理所有的recv(等待发送数据的 goroutine)for { sg := c.recvq.dequeue() // 将缓存队列待处理的recv 拿出来if sg == nil {break}if sg.elem != nil {typedmemclr(c.elemtype, sg.elem)sg.elem = nil}if sg.releasetime != 0 {sg.releasetime = cputicks()}gp := sg.ggp.param = unsafe.Pointer(sg)sg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}glist.push(gp) // 放到 glist}// release all writers (they will panic)for { // 获取所有发送者sg := c.sendq.dequeue()if sg == nil {break}sg.elem = nilif sg.releasetime != 0 {sg.releasetime = cputicks()}gp := sg.ggp.param = unsafe.Pointer(sg)sg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}glist.push(gp)}unlock(&c.lock)// 唤醒所有的glist中的goroutinefor !glist.empty() { gp := glist.pop()gp.schedlink = 0goready(gp, 3)}
}

1- 如果channel 是nil 空指针或者已经关闭,会pnaic

2- close操作会做一些收尾的工作:将 recvq , sendq 数据放到 glist(释放队列)中,统一进行释放掉。

3- 有一个点需要注意,先释放 sendq 中的recver 不会存在问题,向关闭的channel 接收数据,最多返回nil。但是如果recvq 的 sender 就会出现问题,因为不能往关闭 channel 发送数据(panic)

总结

  • channel 总体上分三类:

1- 同步:没有缓存

2- 异步: 有缓存

3- chan struct{} 类型的异步 Channel — struct{} 类型不占用内存空间,不需要实现缓冲区和直接发送

  • 读/写/关闭情况可以下面这个表概括

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/229601.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(NeRF学习)NeRFStudio安装win11

参考&#xff1a; 【深度学习】【三维重建】windows11环境配置tiny-cuda-nn详细教程nerfstudio介绍及在windows上的配置、使用NeRFStudio官网githubRuntimeError: PytorchStreamReader failed reading zip archive: failed finding central directory原因及解决 目录 requireme…

不同角度深入探讨Maya和Blender这两款软件的差异

当我们面对三维建模软件的选择时&#xff0c;许多初学者可能会感到迷茫。今天&#xff0c;我们将从不同角度深入探讨Maya和Blender这两款软件的差异&#xff0c;特别是对于游戏建模领域的用户来说&#xff0c;这将有助于您更好地理解两者之间的区别。 软件授权与开发背景&#…

Python爬虫中的协程

协程 基本概念 协程&#xff1a;当程序执行的某一个任务遇到了IO操作时&#xff08;处于阻塞状态&#xff09;&#xff0c;不让CPU切换走&#xff08;就是不让CPU去执行其他程序&#xff09;&#xff0c;而是选择性的切换到其他任务上&#xff0c;让CPU执行新的任务&#xff…

引导过程的解析以及教程za

bios加电自检------mbr--------grub-------加载内核文件------启动第一个进程 bios的主要作用&#xff1a;检测硬件是否正常&#xff0c;然后根据bios中的启动项设置&#xff0c;去找内核文件 boot开机启动项顺序&#xff0c;你可以把内核文件放在何处&#xff1f; 1.硬盘 …

MySQL将多条数据合并成一条的完整示例

数据库中存的是多条数据&#xff0c;展示的时候需要合并成一条 数据表存储形式如下图 以type分组&#xff0c;type相同的算一条&#xff0c;且保留image和link的所有数据&#xff0c;用groupBy只保留一条数据 解决方案&#xff1a;用GROUP_CONCAT 完整语法如下 group_concat…

基于YOLOv8深度学习的人脸面部表情识别系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

10 个值得收藏的顶级手机数据恢复软件【2024年最新】

手机数据恢复&#xff0c;不要担心&#xff0c;今天就给大家分享10款数据恢复软件&#xff01; 现代人的手机中存储了许多重要数据&#xff0c;如照片、视频、消息、联系人等文件&#xff0c;如果手机损坏或数据丢失&#xff0c;这是一件非常烦恼的事情。此时&#xff0c;一款好…

解决jenkins的Exec command命令不生效,或者执行停不下来的问题

Jenkins构建完后将war包通过 Publish Over SSH 的插件发布到服务器上&#xff0c;在服务器上执行脚本时&#xff0c;脚本中的 nohup 命令无法执行&#xff0c;并不生效&#xff0c;我配置的Exec command命令是后台启动一个war包&#xff0c;并输出日志文件。 nohup java -jar /…

nginx源码分析-4

这一章内容讲述nginx的模块化。 ngx_module_t&#xff1a;一个结构体&#xff0c;用于描述nginx中的各个模块&#xff0c;其中包括核心模块、HTTP模块、事件模块等。这个结构体包含了一些模块的关键信息和回调函数&#xff0c;以便nginx在运行时能够正确地加载和管理这些模块。…

《动手学深度学习》学习笔记 第5章 深度学习计算

本系列为《动手学深度学习》学习笔记 书籍链接&#xff1a;动手学深度学习 笔记是从第四章开始&#xff0c;前面三章为基础知道&#xff0c;有需要的可以自己去看看 关于本系列笔记&#xff1a; 书里为了让读者更好的理解&#xff0c;有大篇幅的描述性的文字&#xff0c;内容很…

算法学习系列(十四):并查集

目录 引言一、并查集概念二、并查集模板三、例题1.合并集合2.连通块中点的数量 引言 这个并查集以代码短小并且精悍的特点&#xff0c;在算法竞赛和面试中特别容易出&#xff0c;对于面试而言&#xff0c;肯定不会让你去写一两百行的代码&#xff0c;一般出的都是那种比较短的…

[GKCTF 2020]ez三剑客-eztypecho

[GKCTF 2020]ez三剑客-eztypecho 考点&#xff1a;Typecho反序列化漏洞 打开题目&#xff0c;发现是typecho的CMS 尝试跟着创建数据库发现不行&#xff0c;那么就搜搜此版本的相关信息发现存在反序列化漏洞 参考文章 跟着该文章分析来&#xff0c;首先找到install.php&#xf…

Unable to connect to Redis server

报错内容&#xff1a; Exception in thread "main" org.redisson.client.RedisConnectionException: java.util.concurrent.ExecutionException: org.redisson.client.RedisConnectionException: Unable to connect to Redis server: 175.24.186.230/175.24.186.230…

使用idea构建父子类springboot项目教程

第一步创建一个父类java项目&#xff08;最外层java项目&#xff09; 1.点击File 然后点击new 再点击Project 2.点击Maven 配置Java版本 再点击next 3.GroupId&#xff1a;包结构&#xff0c;ArtifactId&#xff1a;项目名称&#xff0c;填写完&#xff0c;点击next 4.点击…

IOS - 手机安装包 ipa 常见几种方式

安装 ipa 包的方法有很多中&#xff0c;可以通过不同的软件安装&#xff0c;本文只列出了常用的几种&#xff0c;做个简单的归纳整理 1、iTunes 安装 数据线连接手机之后&#xff0c;会自动连接iTunes&#xff0c;&#xff08;第一次连接的时候会提示是否信任此电脑&#xff0…

基于springboot的火锅店管理系统设计与实现

&#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;一 、设计说明 1.1选题动因 当前…

打造绿色饲养链:河南恩珅德农业引领可持续农业发

在河南恩珅德农业的引领下&#xff0c;可持续农业的概念得到了更进一步的实践和推动。其致力于打造绿色饲养链的努力&#xff0c;旨在通过创新的理念和科技手段&#xff0c;实现饲养业的可持续发展。本文将深入探讨河南恩珅德农业是如何引领可持续农业发展&#xff0c;打造绿色…

Selenium教程06:单选框+多选框+下拉框组件的示例练习

1.Radio单选框的示例用法&#xff0c;通过网页元素class和type属性多条件共同定位元素&#xff0c;模拟依次选中Android&#xff0c;Apple&#xff0c;Windows。 网页元素结构 <input type"radio" class"ivu-radio-input" name"ivuRadioGroup_170…

Flink-【时间语义、窗口、水位线】

1. 时间语义 1.1 事件时间&#xff1a;数据产生的事件&#xff08;机器时间&#xff09;&#xff1b; 1.2 处理时间&#xff1a;数据处理的时间&#xff08;系统时间&#xff09;。 &#x1f330;&#xff1a;可乐 可乐的生产日期 事件时间&#xff08;可乐产生的时间&…

240101-5步MacOS自带软件无损快速导出iPhone照片

硬件准备&#xff1a; iphone手机Mac电脑数据线 操作步骤&#xff1a; Step 1: 找到并打开MacOS自带的图像捕捉 Step 2: 通过数据线将iphone与电脑连接Step 3&#xff1a;iphone与电脑提示“是否授权“&#xff1f; >>> “是“Step 4&#xff1a;左上角选择自己的设…