go channel实践与源码探索(初始化、发送消息、接收消息、关闭)

文章目录

    • 概要
    • 一、并发编程
        • 1.1、Actor模型
        • 1.2、CSP模型
    • 二、Go Channel实践
    • 三、源码分析
        • 3.1、初始化
        • 3.2、发送消息
        • 3.3、接收消息
        • 3.4、关闭通道
    • 总结

概要

通道(Channel)是Go语言提供的协程之间通信与同步的方式。我们知道在并发编程(多进程、多线程)中,进程or线程间的通信和同步是基础,不然就无法解决五位哲学家就餐之类的问题,但是传统的通信同步方式太过复杂,需要开发者了解互斥锁、临界区管理、条件变量控制等,这些东西在Go语言中Channel都帮你做了(屏蔽了),简而言之,你可以放心大胆的用channel去进行不同Goroutine之间的通信与同步,来完成对Goroutine的竞争关系,协作关系的实现。

Go语言是遵循CSP编程模型的,其核心是通过通道来实现通信与同步。在Go语言中借助其调度器,可以高效的实现通道的阻塞与唤醒,进而实现通道的多路复用(Select 操作)。因此Go的并发编程强调不要利用共享内存来进行Goroutine通讯,而应该依靠通讯来共享数据(Do not communicate by sharing memory; instead, share memory by communicating),尽量避免锁和协程争用。

CSP模型示意图

PS:go V1.20.6

一、并发编程

在聊Go channel之前,这里先问一句,Go Channel是用来干什么的?其实在摘要中已经说了,处理并发下多个Goroutine之间的竞争与协作关系,进程线程在并发中同样面临该问题。
进程间通信在学习操作系统一书中就了解到,同一服务器内的进程之间主要有五种:信号、管道、消息传递、信号量、共享内存(当然了,不同服务器进程之间通过网络,比如TCP,本文只聊同一服务器)。
线程之间主要是共享内存+锁、消息传递等。

消息传递和共享内存模式是使用比较广泛的,其中消息传递在实践中主要分为Actor和CSP两种模型,Actor模型重点在于参与交流的实体,而CSP模型重点在于交流的通道。

1.1、Actor模型

Actor模型示意图
Actor模型首先是由Carl Hewitt在1973年提出定义, 随后由Erlang OTP (Open Telecom Platform) 推广开来。Actor属于并发组件模型, 通过组件方式定义并发编程范式的高级原语,避免使开发者直接接触多线程并发或线程池等基础概念,其消息传递更加符合面向对象的原始意图。
传统多数流行的语言并发是基于多线程之间的共享内存,使用同步机制(互斥锁)来防止写竞争。而在Actor消息模型中,每个Actor在任何时间都是串行执行的,即同一时间处理最多一个消息,最多可以发送一个消息给其他Actor,保证了单独写原则,从而巧妙避免了多线程的写竞争。

在Actor模型中,主角是Actor,类似Go语言中的goroutine,Actor彼此之间可以直接发送消息,不需要经过中介,消息是异步发送和处理的。
每个 Actor 可以认为是一个基本的计算单元,它能接收消息并基于其进行运算,也可以发送消息给其他 Actor。Actor之间相互隔离,它们之间并不共享内存。

Actor 是由状态(State)、行为(Behavior)和邮箱(MailBox,可以认为是一个消息队列)三部分组成:

  • 状态:Actor 中的状态指 Actor 对象的变量信息,状态由 Actor 自己管理,避免了并发环境下的锁和内存原子性等问题;
  • 行为:Actor 中的计算逻辑,通过 Actor 接收到的消息来改变 Actor 的状态;
  • 邮箱:邮箱是 Actor 和 Actor 之间的通信桥梁,邮箱内部通过 FIFO消息队列来存储发送方 Actor 消息,接受方 Actor 从邮箱队列中获取消息。

除了Erlang语言,目前akka库是比较流行的Actor并发编程模型的实现,支持Scala和Java语言

1.2、CSP模型

CSP模型示意图
CSP(Communicating Sequential Processes)是由Tony Hoare在1978的论文上首次提出的。 它是处理并发编程的一种设计模式或者模型,指导并发程序的设计,提供了一种并发程序可实践的组织方法或者设计范式。通过此方法,可以减少并发程序引入的其它缺点,减少和规避并发程序的常见缺点和bug,并且可以被数学理论所论证。

CSP将程序分成两种模块,Processor 与 命名Channel:

  • Processor 代表了执行任务的顺序单元,它们内部没有并发;
  • Channel代表了并发流之间的信息交互,如共享数据的交换、修改、消息传递,状态同步等等。

除了Channel,Processor之间再无联系,这样就将并发同步作用转移至Channel,使得问题得到了约束、集中。同步操作与竞争操作并没有消失,只是聚焦于Channel之上。Processor之间的协作,由Channel提供并发原语支持,最初Channel是无缓冲的,因此发送操作会阻塞,直到接收端接收后才能继续发送,从而提供了一种同步机制。

在CSP模型中,Processor 之间不直接彼此联系,而是通过不同Channel进行消息发布和同步。消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。所以CSP 的好处是使得系统较为清晰,Processor 之间是解耦合的,职责也非常清楚,容易理解和维护。

Go语言的并发编程模型参考了 CSP 理论,其中执行单元Processor对应的是Goroutine, 消息通道Channel对应的就是channel,一起通过后续章节,结合通道(Channel)的使用和原理来体会CSP思想的精髓

二、Go Channel实践

  1. 初始化
var cname chan int //声明一个chan int 类型的变量,变量名称叫name,默认值为nil
bname := make(chan int)//make函数初始化channel,无缓冲通道
cname = make(chan int,2)//make函数初始化channel,有缓冲通道,缓冲大小为2

注意:一个只声明但未初始化的chan 变量不能进行读,写,关闭、for遍历操作,否则会panic

  1. 操作
cname<-1 //写入,如果没有缓冲区或缓冲区已满会阻塞
v,ok:=<-cname //读取,如果没有可读数据会阻塞
//或者 v:=<-cname
close(cname)//关闭通道,如果重复关闭会panic
for c:=range cname { //遍历,如果通道没有关闭,在遍历完既有数据后,会一直阻塞fmt.Println("v:%d,v addr:%p\n", v, &v)
}

for…range是一个语法糖,在遍历通道时,其本质如下:

for{v,ok := <-chif!ok{break}original body
}
  1. 多路复用select
    一个select管理多个channel(类似linux网路操作select,poll,epoll,可以同时管理多个socket)
var dname chan int
select {
case <-cname:fmt.Println("rand one")
case c:<-cname:fmt.Println("rand two:", c)	
case <-dname: //通过select 读取未声明的chan 是不会panic的fmt.Println("read nil chan")
case dname<-1: //通过select 写入未声明的chan 是不会panic的,其本质就是占据一次select机会,并不会真的写入数据fmt.Println("write nil chan")	
default:fmt.Println("rand one")//其他分支都被阻塞时才会被执行
}

很多时候我们并不希望select执行一个分支,就退出,这时可以去for + select

cname<-1
for {select {//注意,每一次select,如果多个分支的channel都是可读或写的,那么选择哪一个分支完全是随机的case <-cname:fmt.Println("rand one")case <-cname:fmt.Println("rand two")	
}

三、源码分析

go channel的结构体如下:

type hchan struct {qcount   uint     //  缓冲区中的元素个数dataqsiz uint     // 缓冲区大小buf      unsafe.Pointer // 缓冲区,一个循环队列(数组)sendx    uint   // 队尾,记录发送者在 buf 中索引位置recvx    uint   // 队头,记录接收者在 buf 中索引位置//通道元素相关elemsize uint16  //元素大小elemtype *_type // 元素类型//closed   uint32 //通道关闭标记recvq    waitq  // 阻塞的接收者队列 list of recv waiterssendq    waitq  // 阻塞的发送者队列 list of send waiterslock mutex //互斥锁,并发保护。也协助实现channel的同步作用,即当缓冲区耗尽时阻塞接收者或发送者
}type waitq struct {first *sudog   //记录双向链表的头部last  *sudog   //记录双向链表的尾部
}
//其中sudog表示一个在等待链表中goroutine,比如channel中的recvq和sendq。sudog与goroutine是多对1的,如下:
// sudog is necessary because the g ↔ synchronization object relation
// is many-to-many. A g can be on many wait lists, so there may be
// many sudogs for one g; and many gs may be waiting on the same
// synchronization object, so there may be many sudogs for one object.

根据Go channel结构体可得其组成示意图如下:
go channel结构示意图

... 代表代码省略

3.1、初始化

func makechan(t *chantype, size int) *hchan {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")}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// 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: //当通道元素非指针// 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: //当通道元素是指针// 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) //初始化互斥锁...return c
}
  1. 当缓冲区设置为0时,只需申请hchan结构体大小的内存即可;
  2. 当通道元素非指针时,需要申请hchanSize+mem大小的连续内存,前hchanSize大小用来存储hchan结构体,剩余的用来表示缓冲区;
  3. 当通道元素是指针时,hchan结构体和缓冲区的内存需要分开申请,因为当元素包含指针时,需要单独申请独立内存才能进行正常的垃圾回收。

3.2、发送消息

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {return chansend(c, elem, false, getcallerpc()) //select 方法发送channel消息是非阻塞的
}
func chansend1(c *hchan, elem unsafe.Pointer) {chansend(c, elem, true, getcallerpc()) //正常的发送channel消息走阻塞逻辑,即没有缓冲区可用时就阻塞当前goroutine
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {if c == nil {if !block {return false//走非阻塞模式,向nil chan发消息,返回false表示不可写}gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)//走阻塞模式发消息,就会panic,提示【chan send (nil chan)】throw("unreachable")}if !block && c.closed == 0 && full(c) {return false //走非阻塞模式,向已关闭或缓冲区耗尽的通道发消息,返回false表示不可写}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}lock(&c.lock)    //上锁if c.closed != 0 {unlock(&c.lock)panic(plainError("send on closed channel"))//走阻塞模式,向已关闭的通道发消息,就会panic}//下面就是核心了if sg := c.recvq.dequeue(); sg != nil {//如果接收者队列中有等待读取的goroutine,直接将消息复制给队头协程,并唤醒它// Found a waiting receiver. We pass the value we want to send// directly to the receiver, bypassing the channel buffer (if any).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.qp := chanbuf(c, c.sendx)if raceenabled {racenotify(c, c.sendx, nil)}typedmemmove(c.elemtype, qp, ep)c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++unlock(&c.lock)return true}if !block {//走非阻塞模式,返回false表示不可写unlock(&c.lock)return false}//如果缓冲区被耗尽,走同步模式,则将当前goroutine放入sudog结构中,并放入发送者队列末尾,最终进入休眠状态等待被唤醒// Block on the channel. Some receiver will complete our operation for us.gp := getg()//获取当前协程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 = nilmysg.g = gp    //将当前goroutine放入sudog结构中mysg.isSelect = falsemysg.c = cgp.waiting = mysggp.param = nilc.sendq.enqueue(mysg)//将sudog放入发送者队列末尾gp.parkingOnChan.Store(true)//进行协程切换,去执行别的goroutine,当前goroutine进入休眠状态gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)//等待被唤醒时继续执行后面的代码// Ensure the value being sent is kept alive until the// receiver copies it out. The sudog has a pointer to the// stack object, but sudogs aren't considered as roots of the// stack tracer.KeepAlive(ep)// someone woke us up.if mysg != gp.waiting {throw("G waiting list is corrupted")}gp.waiting = nilgp.activeStackChans = falseclosed := !mysg.success//channel被关闭后该字段值会被设为falsegp.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"))//在睡眠中,该channel被关闭了,再被唤醒,也会panic,一般不会触发,因为close是会先唤醒等待的读写队列后才会设置closed}return true
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
...
goready(gp, skip+1) //将sg中的goroutine放到协程的运行队列中,优先放到当前P的本地运行队列,满了在放到全局运行队列,详情见Go GMP
...
}

对源码分析后,可知发送消息分为阻塞模式和非阻塞模式,我们最常用的是阻塞模式,核心逻辑如下:

  1. 当接收者队列中有等待读取的goroutine时,直接将消息复制给队头协程,并唤醒它。注意这里的唤醒并不是直接切换到队头协程,而是将其放到运行队列中,等待被调度;
  2. 不满足第一步,且当缓冲区未耗尽时,则将消息写入缓冲区队尾;
  3. 不满足第二步,且当缓冲区耗尽时,则将当前goroutine放入sudog结构中,并放入发送者队列sendq末尾,进行协程切换,令当前goroutine进入休眠状态等待被唤醒。

3.3、接收消息

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {return chanrecv(c, elem, false) //select 方法接收channel消息是非阻塞的
}
func chanrecv1(c *hchan, elem unsafe.Pointer) {chanrecv(c, elem, true)//正常的接收channel消息走阻塞逻辑,即缓冲区没有消息时就阻塞当前goroutine
}
//参数解析
//c  hchan结构体
//ep 接收消息的指针,v <- c,那么ep就是v的指针
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {if c == nil {if !block {//走非阻塞模式,向nil chan发消息,返回false表示不可读return}gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)//走阻塞模式发消息,就会panic,提示【chan receive (nil chan)】throw("unreachable")}// Fast path: check for failed non-blocking operation without acquiring the lock.if !block && empty(c) {//非阻塞模式且缓冲区大小为0if atomic.Load(&c.closed) == 0 {//已关闭的通道直接返回return}// The channel is irreversibly closed. Re-check whether the channel has any pending data// to receive, which could have arrived between the empty and closed checks above.// Sequential consistency is also required here, when racing with such a send.if empty(c) {// The channel is irreversibly closed and empty.if raceenabled {raceacquire(c.raceaddr())}if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}}lock(&c.lock)//上锁if c.closed != 0 {if c.qcount == 0 {if raceenabled {raceacquire(c.raceaddr())}unlock(&c.lock)if ep != nil {typedmemclr(c.elemtype, ep)}return true, false //已关闭且缓冲区没有消息的直接返回,所以一个已关闭的channel也是可以读取消息的,但是已关闭的channel是无法写入消息的。}// The channel has been closed, but the channel's buffer have data.} else {// Just found waiting sender with not closed.if sg := c.sendq.dequeue(); sg != nil {  //如果发送者队列中有等待写入的goroutine,获取发送者队列中的队头goroutine,直接将其写入的消息复制给当前协程,并唤醒它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)}if ep != nil {typedmemmove(c.elemtype, ep, qp)//将缓冲区的消息内容复制给承接消息内容的指针}typedmemclr(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}//如果缓冲区没有消息,走同步模式,则将当前goroutine放入sudog结构中,并放入接收者队列末尾,最终进入休眠状态等待被唤醒// no sender available: block on this channel.gp := getg()//获取当前goroutinemysg := 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 = gp //将当前goroutine放入sudog结构中mysg.isSelect = falsemysg.c = cgp.param = nilc.recvq.enqueue(mysg)//将sudog放入接收者队尾// 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.gp.parkingOnChan.Store(true)//进行协程切换,去执行别的goroutine,当前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 recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {...goready(gp, skip+1)//将sg中的goroutine放到协程的运行队列中,优先放到当前P的本地运行队列,满了在放到全局运行队列,详情见Go GMP
}

对源码分析后,可知接收消息分为阻塞模式和非阻塞模式,我们最常用的是阻塞模式,核心逻辑如下:

  1. 当发送者队列有等待写入的goroutine时,获取发送者队列中的队头goroutine,直接将其写入的消息复制给当前协程,并唤醒它。注意这里的唤醒并不是直接切换到队头goroutine,而是将其放到运行队列中,等待被调度,这样就不会影响当前协程的运行;
  2. 不满足第一步,且当缓冲区有消息时,则将缓冲区队头消息复制给当前协程;
  3. 不满足第二步,且当缓冲区满或缓冲区大小为0时,则将当前goroutine放入sudog结构中,并放入接收者队列recvq末尾,进行协程切换,令当前goroutine进入休眠状态等待被唤醒。

3.4、关闭通道

func closechan(c *hchan) {if c == nil {//关闭未初始化的通道会panicpanic(plainError("close of nil channel"))}lock(&c.lock)//上锁if c.closed != 0 {//重复关闭通道会panicunlock(&c.lock)panic(plainError("close of closed channel"))}...c.closed = 1 //设置closed标志位为1var glist gList// release all readersfor {sg := c.recvq.dequeue()if sg == nil {break}...gp := sg.g...glist.push(gp)//收集所有接收者队列中被阻塞的goroutine}// release all writers (they will panic)for {sg := c.sendq.dequeue()if sg == nil {break}...gp := sg.g...glist.push(gp)//收集所有发送者队列中被阻塞的goroutine}unlock(&c.lock)// Ready all Gs now that we've dropped the channel lock.for !glist.empty() {gp := glist.pop()gp.schedlink = 0goready(gp, 3) //统一唤醒接收者队列和发送者队列中被阻塞的goroutine}
}

总结

经过第二、三章节的阅读几乎涵盖Go channel的所有知识点了,这里再多说一下chan是否必须close,其实是没必要的,当没有goroutine持有这个chan时其就会被gc回收,并不会泄漏。close最大的作用是唤醒所有接收者队列和发送者队列中被阻塞的goroutine。

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

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

相关文章

时序分解 | MATLAB实现北方苍鹰优化算法NGO优化VMD信号分量可视化

时序分解 | MATLAB实现北方苍鹰优化算法NGO优化VMD信号分量可视化 目录 时序分解 | MATLAB实现北方苍鹰优化算法NGO优化VMD信号分量可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 北方苍鹰优化算法NGO优化VMD&#xff0c;对其分解层数&#xff0c;惩罚因子数做优化…

基于Python开发的玛丽大冒险小游戏(源码+可执行程序exe文件+程序配置说明书+程序使用说明书)

一、项目简介 本项目是一套基于Python开发的玛丽冒险小游戏程序&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Python学习者。 包含&#xff1a;项目源码、项目文档等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xf…

Alibaba(商品详情)API接口

为了进行电商平台 的API开发&#xff0c;首先我们需要做下面几件事情。 1&#xff09;开发者注册一个账号 2&#xff09;然后为每个alibaba应用注册一个应用程序键&#xff08;App Key) 。 3&#xff09;下载alibaba API的SDK并掌握基本的API基础知识和调用 4&#xff09;利…

管理类联考——数学——汇总篇——知识点突破——应用题——交叉比例法/杠杆原理

读书笔记 甲有&#xff1a;x个a&#xff0c;乙有&#xff1a;y个b&#xff0c;甲乙的平均值为c&#xff0c;根据总数相等&#xff0c;得&#xff1a;axbyc(xy)&#xff0c;即ax-cxcy-by&#xff0c;则 x y c − b a − c \frac{x}{y}\frac{c-b}{a-c} yx​a−cc−b​ &#…

OpenCV(二十五):边缘检测(一)

目录 1.边缘检测原理 2.Sobel算子边缘检测 3.Scharr算子边缘检测 4.两种算子的生成getDerivKernels() 1.边缘检测原理 其原理是基于图像中灰度值的变化来捕捉图像中的边界和轮廓。梯度则表示了图像中像素强度变化的强弱和方向。 所以沿梯度方向找到有最大梯度值的像素&…

17-数据结构-查找-(顺序、折半、分块)

简介&#xff1a;查找&#xff0c;顾名思义&#xff0c;是我们处理数据时常用的操作之一。大概就是我们从表格中去搜索我们想要的东西&#xff0c;这个表格&#xff0c;就是所谓的查找表&#xff08;存储数据的表&#xff09;。而我们怎么设计查找&#xff0c;才可以让计算机更…

sqli-labs关卡之一(两种做法)

目录 一、布尔盲注(bool注入) 二、时间盲注(sleep注入) 一、布尔盲注(bool注入) 页面没有报错和回显信息&#xff0c;只会返回正常或者不正常的信息&#xff0c;这时候就可以用布尔盲注 布尔盲注原理是先将你查询结果的第一个字符转换为ascii码&#xff0c;再与后面的数字比较…

Redis带你深入学习数据类型set

目录 1、set 2、set相关命令 2.1、添加元素 sadd 2.2、获取元素 smembers 2.3、判断元素是否存在 sismember 2.4、获取set中元素数量 scard 2.5、删除元素spop、srem 2.6、移动元素smove 2.7、集合中相关命令&#xff1a;sinter、sinterstore、sunion、sunionstore、s…

mac电脑安装paste教程以及重新安装软件后不能使用解决方法

问题背景 mac电脑安装paste教程以及重新安装软件后不能使用解决方法。 mac电脑安装paste失败&#xff0c;安装好后还是无法使用&#xff0c;paste显示还是历史粘贴信息&#xff0c;导致无法使用。新 copy的内容也无法进入历史粘贴版里面。 笔者电脑配置信息&#xff1a;MacB…

【算法与数据结构】501、LeetCode二叉搜索树中的众数

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;根据前面几篇文章98、LeetCode验证二叉搜索树、530、LeetCode二叉搜索树的最小绝对差。我们知道二叉搜…

Boost搜索引擎

项目背景 先说一下什么是搜索引擎,很简单,就是我们平常使用的百度,我们把自己想要所有的内容输入进去,百度给我们返回相关的内容.百度一般给我们返回哪些内容呢?这里很简单,我们先来看一下. 搜索引擎基本原理 这里我们简单的说一下我们的搜索引擎的基本原理. 我们给服务器发…

Selenium - Tracy 小笔记2

selenium本身是一个自动化测试工具。 它可以让python代码调用浏览器。并获取到浏览器中加们可以利用selenium提供的各项功能。帮助我们完成数据的抓取。它容易被网站识别到&#xff0c;所以有些网站爬不到。 它没有逻辑&#xff0c;只有相应的函数&#xff0c;直接搜索即可 …

基于SSM的精品酒销售管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

Linux平台如何实现采集音视频数据并注入轻量级RTSP服务?

技术背景 好多开发者&#xff0c;问我们最多的问题是&#xff0c;为什么要设计轻量级RTSP服务&#xff1f;轻量级RTSP服务&#xff0c;和RTSP服务有什么区别&#xff1f; 针对这个问题&#xff0c;我们的回答是&#xff1a;轻量级RTSP服务解决的核心痛点是避免用户或者开发者…

MSTP + Eth-Trunk配置实验 华为实验手册

1.1 实验介绍 1.1.1 关于本实验 以太网是当今现有局域网LAN&#xff08;Local Area Network&#xff09;采用的最通用的通信协议标准&#xff0c;以太网作为一种原理简单、便于实现同时又价格低廉的局域网技术已经成为业界的主流。 本实验主要介绍了LAN网络中的Eth-Trunk技术…

Jmeter系列-Jmeter面板介绍和常用配置(2)

Jmeter面板介绍 常用菜单栏 分布式运行相关的 选项&#xff0c;可以打开日志&#xff0c;修改语言、函数助手对话框&#xff0c;还有管理插件 常用的图标 从左到右依次 新建测试计划选择测试计划模板创建一个新的测试计划打开jmeter脚本保存jmeter脚本剪切复制粘贴展开目录…

《向量数据库指南》——向量数据库和关系型数据库的区别?

向量数据库和关系型数据库是两种不同类型的数据库系统,它们在数据模型、数据存储、查询操作等方面存在许多区别。以下是向量数据库和关系型数据库的主要区别: 1、数据模型: 向量数据库:向量数据库专门设计用于存储和查询向量数据,这些数据通常表示为数值向量或嵌入向量。向…

ssm实现折线统计图

​ 方法1&#xff1a;单张数据表中的数据图表生成 图表统计&#xff0c;查看部门人数统计这里实现的时单张表中的数据实现部门人数折线统计图展示。 <script type"text/javascript">// 利用AjAx来获取后台传入的数据&#xff08;Responsebody注解传入的&…

阿里云和腾讯云2核2G服务器价格和性能对比

2核2G云服务器可以选择阿里云服务器或腾讯云服务器&#xff0c;腾讯云轻量2核2G3M带宽服务器95元一年&#xff0c;阿里云轻量2核2G3M带宽优惠价108元一年&#xff0c;不只是轻量应用服务器&#xff0c;阿里云还可以选择ECS云服务器u1&#xff0c;腾讯云也可以选择CVM标准型S5云…

springboot集成Actuator+Prometheus+Grafana

一、环境准备 PrometheusGrafana环境准备 请参考我的博文&#xff1a;https://blog.csdn.net/luckywuxn/article/details/129475991 二、代码准备 我在本次实践中使用的springboot版本是2.6.13,然后在pom.xml文件中增加一下配置 <dependency><groupId>org.sprin…