go|channel源码分析

article/2025/6/17 5:27:21

文章目录

  • channel
    • hchan
    • makechan
    • chansend
    • chanrecv
    • complie
    • closechan

channel

先看一下源码中的说明

At least one of c.sendq and c.recvq is empty, except for the case of an unbuffered channel with a single goroutine blocked on it for both sending and receiving using a select statement, in which case the length of c.sendq and c.recvq is limited only by the size of the select statement.
For buffered channels, also:
c.qcount > 0 implies that c.recvq is empty.
c.qcount < c.dataqsiz implies that c.sendq is empty.

c.sendq和c.recvq至少有一个是empty;有一种情况例外:对于一个无缓冲的chan,在select语句中使用chan收发数据会被single gouroutine阻塞,[演示示例如下],此时c.sendq和c.recv.q的长度由select的size的决定;对于有缓冲的chan:
1.c.qcount>0,表明c.recvq为empty
2.c.qcount<c.dataqsiz表明sendq为empty

  • 对于"unbuffered chan",放在select语句中会被阻塞
    在这里插入图片描述
  • 放在一个goroutine中会产生"dead lock"
    在这里插入图片描述
  • 放在不同的协程中就不会产生dead lock
    在这里插入图片描述
  • 对于nil chan放在同一个协程中和不同的协程中都会被阻塞产生deadlock;放在select语句可以走default分支
  • nil chan只是声明而没有定义,没有为其分配内存;empty chan为其分配内存但是无缓冲

hchan

接下来看一下chan结构体"hchan"的定义

type hchan struct {qcount   uint           // buf中的元素个数dataqsiz uint           // buf的容量buf      unsafe.Pointer // 指向存储数据的底层数组elemsize uint16  //元素大小closed   uint32  //标识chan是否关闭elemtype *_type // element typesendx    uint   // send indexrecvx    uint   // receive indexrecvq    waitq  // list of recv waiterssendq    waitq  // list of send waiters//Lock保护hchan中的所有字段,以及在此通道上阻塞的sudogs中的几个字段。lock mutex
}
type waitq struct {first *sudog //sudog represents a g in a wait list, //such as for sending/receiving on a channel.last  *sudog
}
//只列出了部分字段
type sudog struct {g *gnext *sudogprev *sudog............c        *hchan // channel
}

makechan

利用"make(chan int,1)“创建并初始化一个chan,会调用"makechan”

func makechan(t *chantype, size int) *hchan {//获取chan中存储元素的相关信息elem := t.elem// compiler checks this but be safe.if elem.size >= 1<<16 {throw("makechan: invalid channel element type")}if hchanSize%maxAlign != 0 || elem.align > maxAlign {throw("makechan: bad alignment")}//elem.size表示元素的大小,size表示元素的个数mem, overflow := math.MulUintptr(elem.size, uintptr(size))if overflow || mem > maxAlloc-hchanSize || size < 0 {panic(plainError("makechan: size out of range"))}var c *hchanswitch {case mem == 0://元素大小为0或者元素个数为0// Queue or element size is zero.c = (*hchan)(mallocgc(hchanSize, nil, true))// Race detector uses this location for synchronization.c.buf = c.raceaddr()case elem.ptrdata == 0://元素不包含指针类型的数据,一次性分配hchan和buf// Elements do not contain pointers.// Allocate hchan and buf in one call.c = (*hchan)(mallocgc(hchanSize+mem, nil, true))c.buf = add(unsafe.Pointer(c), hchanSize)default://元素含有指针,需要先为hchan分配内存,然后再为buf分配内存,这样做是为了方便gc回收// Elements contain pointers.c = new(hchan)c.buf = mallocgc(mem, elem, true)}c.elemsize = uint16(elem.size)c.elemtype = elemc.dataqsiz = uint(size)lockInit(&c.lock, lockRankHchan)if debugChan {print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")}return c
}

总结一下分配的方式:

  • 对于无缓冲的chan [size==0]或者chan中存储的元素大小为0[elemsize==0]只分配hchan结构体大小的内存
  • 不含有指针类型的数据,一次性分配hchan+bufsize大小的内存
  • 含有指针类型的数据,先为hchan结构体分配内存,然后再为buf分配内存

chansend

接下来看一下,向chan中发送数据的流程

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {if c == nil {if !block {return false}//当前的协程因向nilchan发送send而被挂起阻塞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:在不获取锁的情况下,检查失败的非阻塞操作full(c)为true的情况:1.无缓冲但是没有等待接收的reciver2.有缓冲但是缓冲通道满了*/if !block && c.closed == 0 && full(c) {return false}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}//上锁lock(&c.lock)if c.closed != 0 {//chan已经被关闭解锁,并报panicunlock(&c.lock)panic(plainError("send on closed channel"))}//c.recvq.dequeue获取recvq的第一个sudogif sg := c.recvq.dequeue(); sg != nil {//如果有正在等待的reciver,可以直接将数据拷贝给该//reciver;绕过chan的缓冲区;拷贝完之后释放锁;//send详解见下面send(c, sg, ep, func() { unlock(&c.lock) }, 3)return true}//有发送数据的位置if c.qcount < c.dataqsiz {// Space is available in the channel buffer. Enqueue the element to send.//chanbuf返回buf中第sendx个 slotqp := chanbuf(c, c.sendx)if raceenabled {racenotify(c, c.sendx, nil)}//将ep拷贝到qptypedmemmove(c.elemtype, qp, ep)//sendx指向下一个发送数据的位置,将其加1c.sendx++//循环数组if c.sendx == c.dataqsiz {c.sendx = 0}//chan中元素个数加1c.qcount++//解锁unlock(&c.lock)return true}if !block {//没有正在等待接收数据的reciver也没有可用的空位置存储数据,在非阻塞模式下直接解锁+返回unlock(&c.lock)return false}// Block on the channel. Some receiver will complete our operation for us./*getg returns the pointer to the current g. The compiler rewrites calls to this function into instructions that fetch the g directly (from TLS or from the dedicated register)返回当前正在运行的goroutine*/gp := getg()//新创建一个sudogmysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}//初始化mysgmysg.elem = epmysg.waitlink = nilmysg.g = gpmysg.isSelect = falsemysg.c = cgp.waiting = mysggp.param = nil//将mysg加入到sendqc.sendq.enqueue(mysg)gp.parkingOnChan.Store(true)//将当前协程挂起,挂起原因是send chan// Puts the current goroutine into a waiting state and calls unlockf on the// system stack.gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)//确保qp对象在reciver接受它之前是有效的KeepAlive(ep)// someone woke us up.if mysg != gp.waiting {throw("G waiting list is corrupted")}gp.waiting = nilgp.activeStackChans = false//chan 被关闭closed := !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
}
/*
full reports 向chan发送数据是否会被阻塞[通道满的情况下会被阻塞]
*/
func full(c *hchan) bool {//c.dataqsiz是一个不可变的字段,创建之后不会被改变。//因此对它的读是并发安全的if c.dataqsiz == 0 {//无缓冲的情况,此时要看是否有等待读取的协程return c.recvq.first == nil}// 通道已满return c.qcount == c.dataqsiz
}

//获取waitq中的一个sudog

func (q *waitq) dequeue() *sudog {for {//获取第一个sudogsgp := q.firstif sgp == nil {return nil}y := sgp.next//waitq中只有一个sudogif y == nil {q.first = nilq.last = nil} else {//ulink and modify first sudog y.prev = nilq.first = ysgp.next = nil // mark as removed (see dequeueSudoG)}/*if a goroutine was put on this queue because of a select, there is a small window between the goroutine being woken up by a different case and it grabbing the channel locks.  Once it has the lock it removes itself from the queue, so we won't see it after that.  We use a flag in the G struct to tell us when someone else has won the race to signal this goroutine but the goroutine hasn't removed itself from the queue yet.如果一个goroutine因为select操作被阻塞而被放入各个case中的waitq[recvq\sendq]中,在这个goroutine被不同的case唤醒到获取chan lock之间有一小段间隙;一旦这个goroutine获取到lock,就会从waitq中移除在g结构体中有一个字段[selectDone]用来通知goroutine1有其它goroutine2已经拿到了这个锁但是goroutine1还没有从waitq中移除。*//*isSelect indicates g is participating in a select, so g.selectDone must be CAS'd to win the wake-up race*///如果没有成功就会直接退出,进入下一个循环if sgp.isSelect && !sgp.g.selectDone.CompareAndSwap(0, 1) {continue}/*这里说一下个人的理解:select 的第一个case对于一个chan1 的recvq,第一个被阻塞的g1是因为等待接收数据而被阻塞的,g1并非当前执行select的协程;select 的第二个case向chan2发送数据,刚开始这两个case都被阻塞,于是这个goroutine被加入到chan1的recvq和chan2的sendq,第二轮循环中,向chan2写数据成功,goroutine被唤醒,然后当前goroutine的selectDone字段被标识为1,与此同时从chan1接收数据成功,但此时会通过selectDone标识判断出当前的g已经被其它case唤醒,于是会继续寻找下一个sudog[当前的goroutine已经从waitq中移除] */return sgp}
}
//向waitq中加入一个sudog
func (q *waitq) enqueue(sgp *sudog) {sgp.next = nilx := q.lastif x == nil {sgp.prev = nilq.first = sgpq.last = sgpreturn}sgp.prev = xx.next = sgpq.last = sgp
}
/*
send在一个empty channel上执行发送操作;发送方发送的ep值被拷贝到接收方的sg中。然后reciver被唤醒;
channel必须是empty或者被锁定。ep必须是非空的
*/
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if raceenabled {if c.dataqsiz == 0 {racesync(c, sg)} else {// Pretend we go through the buffer, even though// we copy directly. Note that we need to increment// the head/tail locations only when 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 {sendDirect(c.elemtype, sg, ep)sg.elem = nil}gp := sg.g//解锁unlockf()gp.param = unsafe.Pointer(sg)sg.success = trueif sg.releasetime != 0 {sg.releasetime = cputicks()}//Mark gp ready to run//标记状态由_Gwaiting到_Grunnable/*先尝试放入本地p的runnext字段,然后是本地runq如果这两个地方都满了,会将其放入全局runq*/goready(gp, skip+1)
}
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {dst := sg.elemtypeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)// No need for cgo write barrier checks because dst is always// Go memory.//将src的数据拷贝到dstmemmove(dst, src, t.size)
}
// chanbuf(c, i) is pointer to the i'th slot in the buffer.
func chanbuf(c *hchan, i uint) unsafe.Pointer {return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}

总结一下发送数据的流程

  • 向nil chan发送数据阻塞模式下会被挂起,非阻塞模式下返回false

  • 首先从recvq中获取是否有正在等待接收数据的goroutine,有的话直接将发送者的数据拷贝到与goroutine绑定的sudog中

  • 没有正在等待接收数据的reciver,但是缓冲区未满将发送者的数据发送到存储数据的循环数组中,并将元素个数加1

  • 既没有正在等待接收数据的reciver也没有可用的缓冲位置,生成新的sudog结构与当前goroutine绑定加入到sendq的末尾,通过gopark将其挂起等待reciver从chan中读取数据

  • 对nil通道close会产生panic
    在真正进行发送数据的流程之前会进行加锁操作,在第一种情况发生之后会释放锁,否则会持续到第二种或者第三种情况;糟糕的情况是,锁一直持续到第三种情况才释放,加大了临界区的范围延长了锁持有的时间,降低了并发性能。
    在这里插入图片描述

  • 通道已经被关闭,向其发送数据会产生panic
    在这里插入图片描述

  • 通道close之后再次被close会被panic
    在这里插入图片描述

chanrecv


/*
chanrecv从chan中接收数据并将数据拷贝到ep中
如果ep是nil,接收到的数据会被忽略
如果block是false并且chan中没有数据会返回false
如果chan被关闭,不影响数据的读取,读取到的数据是类型对应的零值*/
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {// raceenabled: don't need to check ep, as it is always on the stack// or is new memory allocated by reflect.if debugChan {print("chanrecv: chan=", c, "\n")}//从nil chan中获取数据;非阻塞模式下返回false,false//阻塞模式下被挂起if c == nil {if !block {return}gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)throw("unreachable")}//非阻塞模式下//empty(c)返回true的条件//无缓冲通道并且没有正在等待发送数据的sender//有缓冲通道但是c.qcount==0没有数据可以读取if !block && empty(c) {//通道没有被关闭if atomic.Load(&c.closed) == 0 {return}//通道被关闭//empty(c)返回true的条件//无缓冲通道并且没有正在等待发送数据的sender//有缓冲通道但是c.qcount==0没有数据可以读取if empty(c) {// The channel is irreversibly closed and empty.if raceenabled {raceacquire(c.raceaddr())}//清空ep指向的内存if ep != nil {typedmemclr(c.elemtype, ep)}//返回true是因为chan be closed//通道被关闭会通知所有监听该chan的goroutinereturn true, false}}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}//上锁lock(&c.lock)//chan关闭之后不能写入数据但是可以读取数据if c.closed != 0 {//缓冲区中没有数据if c.qcount == 0 {if raceenabled {raceacquire(c.raceaddr())}//没有数据可以读解锁unlock(&c.lock)//清空ep指向的memoryif ep != nil {typedmemclr(c.elemtype, ep)}return true, false}// The channel has been closed, but the channel's buffer have data.} else {//走到这个分支表明缓冲区中有数据并且//发现有阻塞等待发送数据的sender,发生的条件有//无缓冲通道;有缓冲通道但是缓冲区满if sg := c.sendq.dequeue(); sg != nil {//从阻塞等待发送数据的sender中获取数据recv(c, sg, ep, func() { unlock(&c.lock) }, 3)return true, true}}//走到这个分支表明通道被关闭并且缓冲区有数据或者//通道没有被关闭并且有数据可以读取if c.qcount > 0 {// Receive directly from queueqp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)}//将qp中的数据拷贝到epif ep != nil {typedmemmove(c.elemtype, ep, qp)}//清楚qptypedmemclr(c.elemtype, qp)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0}//元素个数减少c.qcount--//解锁unlock(&c.lock)return true, true}//没有数据可以读非阻塞直接返回if !block {//解锁unlock(&c.lock)return false, false}// no sender available: block on this channel.gp := getg()//创建新的sudogmysg := 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 = mysg//与当前goroutine绑定mysg.g = gpmysg.isSelect = falsemysg.c = cgp.param = nil//加入到recvqc.recvq.enqueue(mysg)gp.parkingOnChan.Store(true)//将当前的goroutine挂起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
}
func empty(c *hchan) bool {// c.dataqsiz is immutable.if c.dataqsiz == 0 {return atomic.Loadp(unsafe.Pointer(&c.sendq.first)) == nil}return atomic.Loaduint(&c.qcount) == 0
}
/*
recv在一个full chan[buf is full]上执行接收数据的操作
对于无缓冲通道直接从sender中拷贝数据
对于有缓冲通道
1.将the head of queue的数据拷贝到reciver中
2.将sender的数据发送到缓冲区
*/
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
//无缓冲通道if c.dataqsiz == 0 {if raceenabled {racesync(c, sg)}//直接从sender中拷贝数据if ep != nil {// copy data from senderrecvDirect(c.elemtype, sg, ep)}} else {// Queue is full. Take the item at the// head of the queue. Make the sender enqueue// its item at the tail of the queue. Since the// queue is full, those are both the same slot.//获取接受数数据的slotqp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)racenotify(c, c.recvx, sg)}// copy data from queue to receiverif ep != nil {typedmemmove(c.elemtype, ep, qp)}//空出缓冲区,将sender中的数据发送到缓冲区// copy data from sender to queuetypedmemmove(c.elemtype, qp, sg.elem)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0}//抽取一个元素加入一个元素,buf依旧是满的c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz}sg.elem = nilgp := sg.g//释放锁unlockf()gp.param = unsafe.Pointer(sg)//因chan close被唤醒标记为false;反之标记为truesg.success = trueif sg.releasetime != 0 {sg.releasetime = cputicks()}//将gp由_Gwaiting变为_Grunnable//尝试加入到本地p的runnext,//本地p的runq以及全局的runqgoready(gp, skip+1)
}
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {src := sg.elemtypeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)memmove(dst, src, t.size)
}

总结一下读取数据的流程

  • 对于nil chan,读取数据会被gopark挂起

  • chan be closed并且没有数据可读,返回

  • chan be closed并且有正在等待发送数据的sender
    1.对于无缓冲,直接从sender那里拷贝数据;
    2.对于有缓冲,先从queue的头部获取数据
    然后将sender的数据发送到缓冲区
    将sender的状态由_Gwaiting变为_Grunnable计入到本地p的runnext等待被调度

  • 通道被关闭有数据可读或者没有被关闭但是有数据可读,直接从queue中读取数据,元素个数减少

  • 通道没有被关闭没有数据可以读。创建新的sudog与接收数据的g绑定加入到recvq的末尾,通过gopark挂起等待其它goroutine向chan中发送数据

complie

在select中使用"ch<-i"或者"<-ch"会被编译成if-else语句

// compiler implementsselect {case c <- v:... foodefault:... bar}// asif selectnbsend(c, v) {... foo} else {... bar}
// compiler implementsselect {case v, ok = <-c:... foodefault:... bar}// asif selected, ok = selectnbrecv(&v, c); selected {... foo} else {... bar}

closechan

func closechan(c *hchan) {//关闭nil chan会报错if c == nil {panic(plainError("close of nil channel"))}//上锁lock(&c.lock)//chan 已经被关闭过if c.closed != 0 {//释放锁,报panicunlock(&c.lock)panic(plainError("close of closed channel"))}if raceenabled {callerpc := getcallerpc()racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))racerelease(c.raceaddr())}//将close字段标识为1c.closed = 1//var glist gList// release all readers//通知所有的reader/reciverfor {//从recvq中获取等待读取的sudogsg := c.recvq.dequeue()if sg == nil {//遍历完成break}if sg.elem != nil {//清空sg.elem指向的内存typedmemclr(c.elemtype, sg.elem)//将指针悬空,防止成为野指针sg.elem = nil}if sg.releasetime != 0 {sg.releasetime = cputicks()}//获取与sudog绑定的ggp := sg.ggp.param = unsafe.Pointer(sg)//因为关闭被唤醒设置为falsesg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}//将gp加入到glist中glist.push(gp)}// release all writers (they will panic)//释放所有的sender,并报panic//因为向closed chan发送数据会产生panicfor {//获取sendq中的sendersg := c.sendq.dequeue()if sg == nil {break}sg.elem = nilif sg.releasetime != 0 {sg.releasetime = cputicks()}//获取sudog中的ggp := sg.ggp.param = unsafe.Pointer(sg)//因为close而被唤醒,设置为falsesg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}//将其加入到glist中glist.push(gp)}//唤醒所有的sender和reciver才可以解锁unlock(&c.lock)// Ready all Gs now that we've dropped the channel lock.for !glist.empty() {gp := glist.pop()gp.schedlink = 0//将gp由_Gwaiting变为_Grunnable//尝试加入到本地p的runnext,//本地p的runq以及全局的runqgoready(gp, 3)}
}
  • 通道为nil,报panic: close of nil channel
  • 通道已经关闭(通过closed标识判断),报panic: close of closed channel
  • 唤醒recvq中的所有recver,并将唤醒的g加入到glist
  • 唤醒sendq中的所有sender并报panic: send on closed channel。并将唤醒的g加入到glist

http://www.hkcw.cn/article/BBKRttRLsE.shtml

相关文章

詹俊:国米表现太糟糕了,统治力差距明显

在欧冠决赛中,巴黎圣日耳曼对阵国际米兰,半场结束时巴黎以2-0领先。知名解说员詹俊对比赛进行了点评。詹俊指出,国米差点三球落后,他们在中前场缺乏逼抢力度,使得大巴黎能够轻松控球并通过短传渗透轻易进入进攻区域。大巴黎不仅擅长传控还能快速反击,展现了强大的统治力。…

黄健翔谈大巴黎5-0国米夺欧冠冠军 比赛成一边倒态势

6月1日凌晨,欧冠决赛落幕,巴黎圣日耳曼以5-0战胜国际米兰。赛后,黄健翔在社交媒体上分享了他的看法。他提到自己在赶往机场的途中,边吃早餐边回想刚结束的比赛。他对国米本场比赛的表现感到困惑,特别想了解国米在这场比赛中的战术策略究竟是如何制定的,导致全队多个位置和…

【KWDB 创作者计划】_探秘浪潮KWDB数据库:从时间索引到前沿技术

探秘浪潮KWDB数据库&#xff1a;从时间索引到前沿技术 文章目录 探秘浪潮KWDB数据库&#xff1a;从时间索引到前沿技术引言1.浪潮KWDB数据库时间索引深度解析1.1时间索引工作原理1.2时间索引创建与管理实践 2.浪潮KWDB数据库前沿产品技术纵览2.1多模融合存储引擎2.2就地计算技术…

Linux系统-基本指令(4)

文章目录 touch 指令&#xff08;2-2.25.00&#xff09;知识点&#xff08;普通文件和文本文件&#xff09;mkdir 指令知识点&#xff08;tree指令&#xff09;rm 指令man 指令&#xff08;3-0.00.00&#xff09;知识点&#xff08;Linux下&#xff0c;一切皆为文件&#xff09…

光电设计大赛智能车激光对抗方案分享:低成本高效备赛攻略

一、赛题核心难点与备赛痛点解析 全国大学生光电设计竞赛的 “智能车激光对抗” 赛题&#xff0c;要求参赛队伍设计具备激光对抗功能的智能小车&#xff0c;需实现光电避障、目标识别、轨迹规划及激光精准打击等核心功能。从历年参赛情况看&#xff0c;选手普遍面临三大挑战&a…

力扣 208.实现Trie(前缀树)

文章目录 题目介绍题解 题目介绍 题解 解析&#xff1a; 初始化&#xff1a;创建一棵 26 叉树&#xff0c;一开始只有一个根节点 root。26 叉树的每个节点包含一个长为 26 的儿子节点列表 son&#xff0c;以及一个布尔值 end&#xff0c;表示是否为终止节点。 insert&#xf…

WiFi万能钥匙鲲鹏服务器部署 TiDB 集群实战指南

作者&#xff1a; TiDBer_yangxi 原文来源&#xff1a; https://tidb.net/blog/15a234d0 一、环境准备 1. 硬件要求 服务器架构 &#xff1a;鲲鹏服务器&#xff08;ARM架构&#xff09;&#xff0c;TiDB 官方明确支持 ARM 架构服务器部署 推荐配置 &#xff08;生产环…

Java多线程并发常见问题与解决方案

1. 使用不当:竞争与死锁 synchronized保证了同一时刻只有一个线程访问同步块,但过度使用会导致线程争用、性能瓶颈,甚至死锁。当多个线程在不同顺序上请求多个锁时,容易产生循环等待而死锁。 下面示例演示了两个线程互相持有对方需要的锁导致的死锁情况: Object lockA…

Vue 核心技术与实战day06

1. 路由进阶 1.1 路由的封装抽离 1.21 声明式导航 - 导航链接 1.22 声明式导航 - 两个类名 1.23 声明式导航 - 两个类名 import Find from /views/Find import My from /views/My import Friend from /views/Friendimport Vue from vue import VueRouter from vue-router Vue.…

通信接口 之 串口通信

文章目录 通信接口串口通信硬件电路电平标准串口参数及时序串口时序 通信接口 通信协议及特征 通信目的&#xff1a;将一个设备数据传送到另一个设备&#xff0c;扩展硬件系统&#xff0c;如 STM32 外挂芯片需通信实现数据交换和控制。通信协议作用&#xff1a;制定通信规则&…

2020区块链大作业:基于区块链的供应链金融平台

2020区块链大作业&#xff1a;基于区块链的供应链金融平台 【下载地址】2020区块链大作业基于区块链的供应链金融平台 探索区块链在供应链金融的创新应用&#xff0c;本项目基于区块链技术构建了一个功能丰富的供应链金融平台。不仅实现了基础的金融功能&#xff0c;更通过优化…

以太坊是真正的健全货币吗?驳斥比特币极端主义者的误解

比特币极端主义者通常声称&#xff0c;以太坊&#xff08;ETH&#xff09;没有价值&#xff0c;也不是健全货币。他们认为以太坊的基本面不如比特币&#xff08;BTC&#xff09;。然而&#xff0c;以太坊支持者通过强有力的论据反驳这些观点&#xff0c;强调以太坊不断发展的经…

股指期货交割日对股市有哪些影响?

股指期货交割日可能影响股市&#xff0c;因为这时资金流动增加、对冲交易集中&#xff0c;且期货市场的套利行为和市场情绪的变化都可能导致股市短期内的波动。 股指期货交割日效应 我们先说&#xff0c;股指期货的交割日的效应主要是指股指期货的交割日常常会伴随着市场的波动…

Java 大视界 -- Java 大数据在智能家居能源区块链交易与管理中的应用探索(252)

💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也期待你毫无保留地分享独特见解,愿我们于此携手成长,共赴新程!💖 全网…

资产是什么,有那些,普通人可以获得什么?

资产是什么&#xff1f; 资产是指能够带来经济利益的资源&#xff0c;这些资源被企业或个人所拥有或控制&#xff0c;预期在未来能够带来经济利益流入。简单来说&#xff0c;资产就是能够帮助你增加财富、产生收入或减少支出的东西。 资产有哪些类型&#xff1f; 资产可以分为…

跨链代币开发:架起区块链未来的桥梁

跨链代币开发&#xff1a;架起区块链未来的桥梁 ——多链互操作时代的价值流通革命 一、技术突破&#xff1a;构建跨链代币的四大基石 1️⃣ 跨链桥&#xff1a;资产流通的“超级渡轮” 跨链桥通过锁仓与铸造机制实现资产跨链转移&#xff0c;例如Wrapped Bitcoin&#xff08;W…

poet3 Alpha积分总差一口气?3分钟学会高效刷分技巧!

今天给大家说一套刷积分简单快速有效的刷分流程&#xff1a; 首先刷积分最快捷的方法就是选择&#xff1a;PORT3高效、低损耗刷 为什要选择PORT3了&#xff0c;有何优势值得选择PORT3&#xff01; 方法1&#xff1a; 推荐方式&#xff1a;使用 Binance Wallet 无私钥地址参与…

AI炼丹日志-25 - OpenAI 开源的编码助手 Codex 上手指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇&#xff1a; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 大数据篇 300&#xff1a; Hadoop&…

11.springCloud AlibabaNacos服务注册和配置中心

总体介绍&#xff1a; Nacos简介 Nacos 是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集&#xff0c;帮…

32所新大学来了 有何深意 职业本科加速扩容

职业本科高校正在加速扩容。近日,教育部发布公示,拟同意设置安徽职业技术大学、宁夏职业技术大学、苏州职业技术大学等32所学校。此次拟同意设置的本科高校中,23所为职业本科高校,均由高职专科升级而来,均为公办。自2019年以来,教育部已批准设立了60所本科层次职业学校。…