golang channel执行原理与代码分析
使用的go版本为 go1.21.2
首先我们写一个简单的chan调度代码
package mainimport "fmt"func main() {ch := make(chan struct{})go func() {ch <- struct{}{}ch <- struct{}{}}()fmt.Println("xiaochuan", <-ch)data, ok := <-chfmt.Println("xiaochuan", data, ok)close(ch)
}
 
因为ch的数据获取方式有两种,所以这个示例代码写了两次的ch读与写
 老样子通过go build -gcflags -S main.go获取到对应的汇编代码

调度make最终被转换为CALL runtime.makechan

调度ch <- struct{}{}最终被转换为CALL runtime.chansend1 由于我们调度了两次所以这里有两个
 调度 <-ch 最终被转换为CALL runtime.chanrecv1

我们还进行一次两个参数的调度接收ch读取
 data, ok := <-ch最终被转换为CALL runtime.chanrecv2 
 调度 close(ch) 最终被转换为CALL runtime.closechan 先来看一下hchan构造体相关的底层源码
hchan结构体
//代码位于 GOROOT/src/runtime/chan.go L:33
type hchan struct {qcount   uint           // 环形队列中元素个数dataqsiz uint           // 环形队列的大小buf      unsafe.Pointer // 指向大小为 dataqsiz 的数组elemsize uint16         // 元素大小closed   uint32         // 是否关闭elemtype *_type         // 元素类型sendx    uint           // 发送索引recvx    uint           // 接收索引recvq    waitq          // recv 等待列表,即( <-ch )sendq    waitq          // send 等待列表,即( ch<- )lock     mutex          // 锁
}type waitq struct { // 等待队列 sudog 双向队列first *sudoglast  *sudog
}type sudog struct {// 下面的字段由 sudog 阻塞的 channel 的 hchan.lock 保护。// shrinkstack 依赖这个字段来处理参与 channel 操作的 sudog。g *gnext *sudogprev *sudogelem unsafe.Pointer // 数据元素(可能指向堆栈)// 下面的字段在任何情况下都不会并发访问。// 对于 channels,waitlink 只有 g 访问。// 对于 semaphores,所有字段(包括上面的字段)// 仅在持有 semaRoot 锁时才会访问。acquiretime int64releasetime int64ticket      uint32// isSelect 表示 g 参与了 select,因此 g.selectDone 必须进行 CAS 操作以赢得唤醒竞争。isSelect bool// success 表示通信是否成功。如果 goroutine 被唤醒是因为在通道 c 上传递了值,则为 true,// 如果是因为 c 被关闭而唤醒,则为 false。success boolparent   *sudog // semaRoot 二叉树waitlink *sudog // g.waiting 列表或 semaRootwaittail *sudog // semaRootc        *hchan // channel
} 
先从创建chan开始
makechan源码与解读
//代码位于 GOROOT/src/runtime/chan.go L:65//如果我们make的初始化缓冲区比较大会调度这个函数
func makechan64(t *chantype, size int64) *hchan {//将size强转为int类型//因为go的int类型的大小在不同平台上可能是 32 位或 64 位//如果大小超过了当前平台int最大值,会截断掉超出最大值的部分if int64(int(size)) != size {panic(plainError("makechan: size out of range"))}//强制转换为int类型超出int部分截断return makechan(t, int(size))
}func makechan(t *chantype, size int) *hchan {elem := t.Elem//编辑器检测元素的大小会不会大于2的16次方,对齐方式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"))}//初始化hchanvar c *hchanswitch {case mem == 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.PtrBytes == 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: //元素包含指针// 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 { //开启debug开关,公屏打印print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")}return c
}
 
chansend1源码与解读
//代码位于 GOROOT/src/runtime/chan.go L:142
//c <- x 调度这个函数
func chansend1(c *hchan, elem unsafe.Pointer) {chansend(c, elem, true, getcallerpc())
}func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {if c == nil { //判断当前ch是不是一个空指针,如果为空将当前G休眠,触发崩溃if !block {return false}gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)throw("unreachable")}if debugChan { //开启debug开关,公屏打印print("chansend: chan=", c, "\n")}if raceenabled {//竞争开启racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))}//在无锁的情况下,检测一下是否ch 是否关闭,是否会造成阻塞if !block && c.closed == 0 && full(c) {return false}var t0 int64if blockprofilerate > 0 {t0 = cputicks()}lock(&c.lock) //获取chan锁if c.closed != 0 { // 二次确认chan是不是已经关闭unlock(&c.lock)panic(plainError("send on closed channel"))}//判断当前ch是否存在接收方//如果存在直接调用send函数将数据发送给对方,避免数据复制到缓存区中去if sg := c.recvq.dequeue(); sg != nil { send(c, sg, ep, func() { unlock(&c.lock) }, 3)return true}//判断当前ch元素个数是否小于队列的长度//如果有剩余空间将数据将要发送的元素加入队列if c.qcount < c.dataqsiz {// 获取环形队列中的元素qp := chanbuf(c, c.sendx)if raceenabled {racenotify(c, c.sendx, nil)}// 直接ep复制给qptypedmemmove(c.elemtype, qp, ep)c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++unlock(&c.lock)return true}if !block {unlock(&c.lock)return false}gp := getg()    //获取当前G//获取一个sudog, 优先从P中获取//如果P中的sudog缓存区(本地无锁)为空//从调度器层的sudog缓冲区(全局需要加锁)中拿数据放入P的sudog缓存区mysg := acquireSudog() mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}//将sudog写入send环形队列中去mysg.elem = epmysg.waitlink = nilmysg.g = gpmysg.isSelect = falsemysg.c = cgp.waiting = mysggp.param = nilc.sendq.enqueue(mysg)//将当前G的parkingOnChan设置为true(表示目前停止在了chansend或chanrecv上)//将当前的G移出调度队列(调度chanparkcommit解锁当前ch)gp.parkingOnChan.Store(true)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)//调度KeepAlive函数确保发送的元素处于一个可达的状态避免被回收KeepAlive(ep)//当前后续唤醒G//判断G的等待列表是否为当前的sudog//如果不一致说明G已经被改写了if mysg != gp.waiting {throw("G waiting list is corrupted")}//清空G的等待队列,//获取当前被唤醒的原因sudog.succes//因为唤醒方式有两种,1。通道关闭 2.接收唤起gp.waiting = nilgp.activeStackChans = falseclosed := !mysg.successgp.param = nil //清空G的参数列表if mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}mysg.c = nilreleaseSudog(mysg) //释放sudog重新放回P的sudogcache(本地)if closed { //由于不能写入关闭的chan,所以直接异常了if c.closed == 0 {throw("chansend: spurious wakeup")}panic(plainError("send on closed channel"))}return true
}
 
直接发送的时候调用的send函数解读如下
send源码与解读
//代码位于 GOROOT/src/runtime/chan.go L:295func 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}}// 检测数据是否为空// 如果不为空直接调用sendDirect函数发送数据,然后将其重置为nilif sg.elem != nil {sendDirect(c.elemtype, sg, ep)sg.elem = nil}//获取等待列表中的G,//将当前的ch解锁, sugo赋值为G当做启动参数gp := sg.gunlockf()gp.param = unsafe.Pointer(sg)sg.success = true//sugo判断释放时间是否为0//为0将其设置为当前 CPU 的时钟滴答数if sg.releasetime != 0 {sg.releasetime = cputicks()}//将G标记为可运行状态,放入调度队列等待被后续调度goready(gp, skip+1)
}
 
chanrecv1与chanrecv2源码与解读
//代码位于 GOROOT/src/runtime/chan.go L:442//chanrecv1与chanrecv2的处理逻辑基本差不多
//chanrecv2多接受了一个变量而已 
//可以理解为这样ok := chanrecv2(ch, v)
func chanrecv1(c *hchan, elem unsafe.Pointer) {chanrecv(c, elem, true)
}func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {_, received = chanrecv(c, elem, true)return
}func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {if debugChan {//开启debug开关,公屏打印print("chanrecv: chan=", c, "\n")}if c == nil {//判断当前ch是不是为空指针,如果为空将当前G休眠,触发崩溃if !block {return}gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2)throw("unreachable")}if !block && empty(c) {//非阻塞情况下, 且数据队列为空if atomic.Load(&c.closed) == 0 { //原子读取 当前ch是否关闭,如果关闭直接返回// Because a channel cannot be reopened, the later observation of the channel// being not closed implies that it was also not closed at the moment of the// first observation. We behave as if we observed the channel at that moment// and report that the receive cannot proceed.return}if empty(c) {// 重新检测是否为空ch// The channel is irreversibly closed and empty.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锁if c.closed != 0 {  // 二次确认ch是不是已经关闭if c.qcount == 0 {if raceenabled {raceacquire(c.raceaddr())}unlock(&c.lock)if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}} else {// 判断当前ch是否存在发送方// 如果存在直接调用recv函数将数据接受对方的数据if sg := c.sendq.dequeue(); sg != nil {// Found a waiting sender. If buffer is size 0, receive value// directly from sender. Otherwise, receive from head of queue// and add sender's value to the tail of the queue (both map to// the same buffer slot because the queue is full).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)}if ep != nil {// 直接qp复制给eptypedmemmove(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}gp := getg()//获取当前G//获取一个sudog, 优先从P中获取//如果P中的sudog缓存区(本地无锁)为空//从调度器层的sudog缓冲区(全局需要加锁)中拿数据放入P的sudog缓存区mysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}//将sudog写入recvq环形队列中去mysg.elem = epmysg.waitlink = nilgp.waiting = mysgmysg.g = gpmysg.isSelect = falsemysg.c = cgp.param = nilc.recvq.enqueue(mysg)//将当前G的parkingOnChan设置为true(表示目前停止在了chansend或chanrecv上)//将当前的G移出调度队列(调度chanparkcommit解锁当前ch)gp.parkingOnChan.Store(true)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)//当前后续唤醒G//判断G的等待列表是否为当前的sudog//如果不一致说明G已经被改写了if mysg != gp.waiting {throw("G waiting list is corrupted")}//清空G的等待队列,//获取当前被唤醒的原因sudog.succes//因为唤醒方式有两种,1。通道关闭 2.发送唤起gp.waiting = nilgp.activeStackChans = falseif mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}success := mysg.successgp.param = nilmysg.c = nilreleaseSudog(mysg)//释放sudog重新放回P的sudogcache(本地)return true, success
}
 
直接读取的时候调用的recv函数解读如下
recv源码与解读
//代码位于 GOROOT/src/runtime/chan.go L:616
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {//判断当前环形队列是否为0//为0从发送方复制数据(调度recvDirect函数)if c.dataqsiz == 0 { if raceenabled {racesync(c, sg)}if ep != nil {// copy data from senderrecvDirect(c.elemtype, sg, ep)}} else {// 获取环形队列中的元素qp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)racenotify(c, c.recvx, sg)}// 如果数据不为空 直接ep复制给qpif ep != nil {typedmemmove(c.elemtype, ep, qp)}// 清除数据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}//获取等待列表中的G,//将当前的ch解锁, sugo赋值为G当做启动参数sg.elem = nilgp := sg.gunlockf()gp.param = unsafe.Pointer(sg)sg.success = true//sugo判断释放时间是否为0//为0将其设置为当前 CPU 的时钟滴答数if sg.releasetime != 0 {sg.releasetime = cputicks()}//将G标记为可运行状态,放入调度队列等待被后续调度goready(gp, skip+1)
}
 
closechan源码与解读
//代码位于 GOROOT/src/runtime/chan.go L:358func closechan(c *hchan) {if c == nil {//如果ch未初始化直接报错panic(plainError("close of nil channel"))}lock(&c.lock) //获取chan锁if c.closed != 0 { //如果当前ch已经处于关闭状态,触发异常unlock(&c.lock)panic(plainError("close of closed channel"))}if raceenabled { //竞争开启callerpc := getcallerpc()racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))racerelease(c.raceaddr())}c.closed = 1 //将当前ch设置为关闭状态//待唤醒的G列表var glist gList// release all readersfor { //逐步从读取队列取值,直到获取完为止sg := c.recvq.dequeue()if sg == nil {break}//数据不为空,释放掉对应的内存块if sg.elem != nil {typedmemclr(c.elemtype, sg.elem)sg.elem = nil}// 重置释放时间if sg.releasetime != 0 {sg.releasetime = cputicks()}// 获取对应的G, 重置唤醒参数// 将这个G加入到glist中等待后续唤醒gp := sg.ggp.param = unsafe.Pointer(sg)sg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}glist.push(gp)}for {//逐步从发送队列取值,直到获取完为止 (向关闭的ch发送数据会有panic)sg := c.sendq.dequeue()if sg == nil {break}sg.elem = nil// 重置释放时间if sg.releasetime != 0 {sg.releasetime = cputicks()}// 获取对应的G, 重置唤醒参数// 将这个G加入到glist中等待后续唤醒gp := sg.ggp.param = unsafe.Pointer(sg)sg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}glist.push(gp)}unlock(&c.lock)// 循环glist待唤醒列表将G设置为read状态(唤醒G运行干活)for !glist.empty() {gp := glist.pop()gp.schedlink = 0goready(gp, 3)}
}
 
总结
我们从上面的源码分析了解chan的数据结构、发送数据、接收数据和关闭这些基本操作,从源码分析我们得知chan的读写操作是会上锁的,如果业务中对性能要求比较高的情况下chan的这把锁会成为我们系统内的瓶颈。
相关文章:
golang channel执行原理与代码分析
使用的go版本为 go1.21.2 首先我们写一个简单的chan调度代码 package mainimport "fmt"func main() {ch : make(chan struct{})go func() {ch <- struct{}{}ch <- struct{}{}}()fmt.Println("xiaochuan", <-ch)data, ok : <-chfmt.Println(&…...
OpenCvSharp从入门到实践-(04)色彩空间
目录 1、GRAY色彩空间 2、从BGR色彩空间转换到GRAY色彩空间 2.1色彩空间转换码 2.2实例 BGR色彩空间转换到GRAY色彩空间 3、HSV色彩空间 4、从BGR色彩空间转换到HSV色彩空间 4.1色彩空间转换码 4.2实例 BGR色彩空间转换到HSV色彩空间 1、GRAY色彩空间 GRAY色彩空间通常…...
100.有序数组的平方(力扣)
代码解决一 class Solution { public:// 函数接受一个整数数组,返回每个元素平方值排序后的结果vector<int> sortedSquares(vector<int>& nums) {int len nums.size(); // 获取数组的长度vector<int> v; // 创建一个新的数组,用…...
微服务--01--简介、服务拆分原则
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 微服务微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。 单体架构微服务架构…...
IntelliJ IDEA安装使用教程
IntelliJ IDEA是一个流行的Java 集成开发环境(IDE),由JetBrains公司开发。它是一款全功能的IDE,支持多种编程语言,如Java、Kotlin、Groovy、Scala、Python、JavaScript、HTML、CSS等等。IntelliJ IDEA 提供了高效的代码…...
校园门禁可视化系统解决方案
随着科技的持续进步,数字化校园在教育领域中的地位日益上升,各种智能门禁、安防摄像头等已遍布校园各个地方,为师生提供安全便捷的通行体验。然而数据收集分散、缺乏管理、分析困难等问题也逐渐出现,在这个数字化环境中࿰…...
rest_framework_django学习笔记一(序列化器)
rest_framework_django学习笔记一(序列化器) 一、引入Django Rest Framework 1、安装 pip install djangorestframework2、引入 INSTALLED_APPS [...rest_framework, ]3、原始RESTful接口写法 models.py from django.db import models 测试数据 仅供参考 INSERT INTO de…...
面试题:什么是负载均衡?常见的负载均衡策略有哪些?
文章目录 一、负载均衡二、负载均衡模型分类三、CDN负载均衡四、LVS负载均衡4.1 LVS 支持的三种模式4.1.1 DR 模式4.1.2 TUN 模式4.1.3 NAT 模式 4.2 LVS 基于 Netfilter 的框架实现 五、负载均衡策略是什么六、常用负载均衡策略图解6.1 轮询6.2 加权轮询6.3 最少连接数6.4 最快…...
精通Git(第2版)读书笔记
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言第 1章 入门 11.1 关于版本控制 11.1.1 本地版本控制系统 1 第 2章 Git基础 132.1 获取Git仓库 132.1.1 在现有中初始化Git仓库 132.1.2 克隆现有仓库 14 2.2 在…...
XUbuntu22.04之OBS30.0设置录制音频降噪(一百九十六)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒…...
渗透测试学习day4
文章目录 靶机:SequelTask1Task2Task3Task4Task5Task6Task7Task8 靶机:CrocodileTask1Task2Task3Task4Task5Task6Task7Task8Task9Task10 靶机:ResponderTask1Task2Task3Task4Task5Task6Task7Task8Task9Task10Task11 靶机:ThreeTas…...
Deepin使用记录-deepin系统下安装RabbitMq
目录 0、引言 1、由于RabbitMq是erlang语言开发的,所有需要先安装erlang 2、更新源并安装RabbitMq 3、安装完成之后,服务是启动的,可以通过以下语句查看状态 4、这样安装完成之后,是看不到web页面的,需要再安装一…...
【腾讯云云上实验室】用向量数据库——实现高效文本检索功能
文章目录 前言Tencent Cloud VectorDB 简介Tencent Cloud VectorDB 使用实战申请腾讯云向量数据库腾讯云向量数据库使用步骤腾讯云向量数据库实现文本检索 结论和建议 前言 想必各位开发者一定使用过关系型数据库MySQL去存储我们的项目的数据,也有部分人使用过非关…...
Pytorch中的gather的理解和用法
Pytorch中的gather的理解和用法 这个Gather的用法花费了点时间,我相信很多人一开始不太懂。 跟着我简单理解。 首先样例是: tensor([[ 3, 4, 5],[ 6, 7, 8],[ 9, 10, 11]])然后index: [[2, 1, 0]]然后执行的代码: tensor_0.gather(0…...
唯创知音WTN6系列语音芯片:高音频采样率与精细音量控制赋能广泛应用
在语音芯片领域,唯创知音的WTN6系列语音芯片以其出色的性能和广泛的应用领域,无疑是行业的一颗璀璨明星。近期,该系列芯片实现了音频采样率32kHz的突破,以及16级音量控制的精细调节,进一步提升了其在各类应用中的表现。…...
机器人分类
从发展阶段分类: 1第一代机器人2第二代机器人3第三代机器人:智能型机器人。生于90年代。具有传感器,以前的机器人都不具有传感器 从控制方式分类:(我觉得这个分类好乱) 操作型机器人:可自动控…...
html/css中位置position的绝对位置absolute顺时针盒子案例图片排序
目标图片: Dreamweaver界面: 代码部分: <!doctype html> <html> <head> <meta charset"utf-8"> <title>无标题文档</title> <style type"text/css">.red{background-color:r…...
分享86个清新唯美PPT,总有一款适合您
分享86个清新唯美PPT,总有一款适合您 86个清新唯美PPT下载链接:https://pan.baidu.com/s/1QEaXeWAekCbAWDD0iTgvMw?pwd8888 提取码:8888 Python采集代码下载链接:采集代码.zip - 蓝奏云 学习知识费力气,收集整…...
虚拟机系列:Oracle VM VirtualBox安装/更新/卸载出现 无法访问你试图使用的功能所在的网络位置
Oracle VM VirtualBox安装/更新/卸载出现 无法访问你试图使用的功能所在的网络位置 Oracle VM VirtualBox安装/更新/卸载出现 无法访问你试图使用的功能所在的网络位置Oracle VM VirtualBox安装/更新/卸载出现 无法访问你试图使用的功能所在的网络位置 在更新Oracle VM Virtua…...
【数据库】数据库并发控制的冲突检测,冲突可串行化的调度,保障事务的特性
冲突可串行化 专栏内容: 手写数据库toadb 本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。 本专栏会定期更新…...
盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...
PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
css3笔记 (1) 自用
outline: none 用于移除元素获得焦点时默认的轮廓线 broder:0 用于移除边框 font-size:0 用于设置字体不显示 list-style: none 消除<li> 标签默认样式 margin: xx auto 版心居中 width:100% 通栏 vertical-align 作用于行内元素 / 表格单元格ÿ…...
初探Service服务发现机制
1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能:服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...
vue3 daterange正则踩坑
<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...
