一文图解Golang管道Channel

在这里插入图片描述
在 Go 语言发布之前,很少有语言从底层为并发原语提供支持。大多数语言还是支持共享和内存访问同步到 CSP 的消息传递方法。Go 语言算是最早将 CSP 原则纳入其核心的语言之一。内存访问同步的方式并不是不好,只是在高并发的场景下有时候难以正确的使用,特别是在超大型,巨型的程序中。基于此,并发能力被认为是 Go 语言天生优势之一。追其根本,还是因为 Go 基于 CSP 创造出来的一系列易读,方便编写的并发原语。

不要通过共享内存进行通信建议通过通信来共享内存。(Do not communicate by sharing memory; instead, share memory by communicating)这是 Go 语言并发的哲学座右铭。相对于使用 sync.Mutex 这样的并发原语。虽然大多数锁的问题可以通过 channel 或者传统的锁两种方式之一解决,但是 Go 语言核心团队更加推荐使用 CSP 的方式。

行文目录

在这里插入图片描述

channel的使用场景

把channel用在数据流动的地方

  1. 消息传递、消息过滤
  2. 信号广播
  3. 事件订阅与广播
  4. 请求、响应转发
  5. 任务分发
  6. 结果汇总
  7. 并发控制
  8. 同步与异步

核心数据结构

在这里插入图片描述

hchan

type hchan struct {qcount   uint           // total data in the queuedataqsiz uint           // size of the circular queuebuf      unsafe.Pointer // points to an array of dataqsiz elementselemsize uint16closed   uint32elemtype *_type // element typesendx    uint   // send indexrecvx    uint   // receive indexrecvq    waitq  // list of recv waiterssendq    waitq  // list of send waiterslock mutex
}

hchan: channel 数据结构

  1. qcount:当前 channel 中存在多少个元素;
  2. dataqsize: 当前 channel 能存放的元素容量;
  3. buf:channel 中用于存放元素的环形缓冲区;
  4. elemsize:channel 元素类型的大小;
  5. closed:标识 channel 是否关闭;
  6. elemtype:channel 元素类型;
  7. sendx:发送元素进入环形缓冲区的 index;
  8. recvx:接收元素所处的环形缓冲区的 index;
  9. recvq:因接收而陷入阻塞的协程队列;
  10. sendq:因发送而陷入阻塞的协程队列;

waitq

type waitq struct {first *sudoglast  *sudog
}

waitq:阻塞的协程队列

  1. first:队列头部
  2. last:队列尾部

sudog

type sudog struct {g *gnext *sudogprev *sudogelem unsafe.Pointer // data element (may point to stack)// ...c        *hchan 
}

sudog:用于包装协程的节点

  1. g:goroutine,协程;
  2. next:队列中的下一个节点;
  3. prev:队列中的前一个节点;
  4. elem: 读取/写入 channel 的数据的容器;
  5. c:标识与当前 sudog 交互的 chan;

构造器函数

在这里插入图片描述
这里分成三种,无缓冲型的channel,struct类型的有缓冲的以及pointer类型的有缓冲的

创建 channel 常见代码:

ch := make(chan int)

在底层会调用makechan64() 或者 makechan() ,这里分析makechan方法

func makechan(t *chantype, size int) *hchan {elem := t.elem// ...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:// Queue or element size is zero.c = (*hchan)(mallocgc(hchanSize, nil, true))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
}
  1. 首先判断申请内存空间大小是否越界,mem 大小为 element 类型大小与 element 个数相乘后得到,仅当无缓冲型 channel 时,因个数为 0 导致大小为 0;
  2. 根据类型,初始 channel,分为无缓冲型(mem为0)、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
  3. 倘若为无缓冲型,则仅申请一个大小为默认值 96 (hchanSize)的空间;
  4. 如若有缓冲的 struct 型,则一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
  5. 倘若为有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续
  6. 对 channel 的其余字段进行初始化,包括元素类型大小、元素类型、容量以及锁的初始化。

发送数据写流程

向 channel 中发送数据常见代码:

ch <- 1

那么会实际调用chansend1 -> chansend方法,这里介绍下。

两类异常情况处理

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 {gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)throw("unreachable")}lock(&c.lock)if c.closed != 0 {unlock(&c.lock)panic(plainError("send on closed channel"))}
}
  1. 对于未初始化的 chan,写入操作会引发死锁;
  2. 对于已关闭的 chan,写入操作会引发 panic.

case1:写时存在阻塞读协程(同步发送)

在这里插入图片描述
前提:写,存在阻塞读的协程,说明要么无缓冲,要么缓冲区满了假设同步发送没有缓冲区,将当前写协程加入到队列里面。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {// ...lock(&c.lock)// ...if c.closed != 0 {unlock(&c.lock)panic(plainError("send on closed channel"))}if sg := c.recvq.dequeue(); sg != nil {// 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}// ..
}
  1. 首先进行加锁操作,保证线程安全;
  2. 并再一次检查 channel 是否关闭。如果关闭则抛出 panic
  3. 从阻塞调度的读协程队列的头部中取出第一个非空的 goroutine 的封装对象 sudog;
  4. 在 send 方法中,会基于 memmove 方法,直接将元素拷贝交给 sudog 对应的 goroutine;
  5. 在 send 方法中会完成解锁动作.
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if sg.elem != nil {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()}goready(gp, skip+1)
}

send() 函数主要完成了 2 件事:

  • 调用 sendDirect() 函数将数据拷贝到了接收变量的内存地址上
  • 调用 goready() 将等待接收的阻塞 goroutine 的状态从 Gwaiting 或者 Gscanwaiting 改变成 Grunnable。下一轮调度时会唤醒这个接收的 goroutine。

在这里插入图片描述

case2:写时无阻塞读协程且环形缓冲区仍有空间(异步发送)

在这里插入图片描述
前提:缓冲区还有空闲位置,能够直接将数据写入缓冲区。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {// ...lock(&c.lock)// ...if c.qcount < c.dataqsiz {// Space is available in the channel buffer. Enqueue the element to send.qp := chanbuf(c, c.sendx)typedmemmove(c.elemtype, qp, ep)c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++unlock(&c.lock)return true}// ...
}
  1. 首先进行加锁操作,保证线程安全;
  2. 将当前元素添加到环形缓冲区 sendx 对应的位置;
  3. sendx++;
  4. qcount++;
  5. 解锁,返回。

case3:写时无阻塞读协程且环形缓冲区无空间(阻塞发送)

在这里插入图片描述
前提:当 channel 处于打开状态,但是没有接收者,并且没有 buf 缓冲队列或者 buf 队列已满,这时 channel 会进入阻塞发送。

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {// ...lock(&c.lock)// ...gp := getg()mysg := acquireSudog()mysg.elem = epmysg.g = gpmysg.c = cgp.waiting = mysgc.sendq.enqueue(mysg)atomic.Store8(&gp.parkingOnChan, 1)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)gp.waiting = nilclosed := !mysg.successgp.param = nilmysg.c = nilreleaseSudog(mysg)return true
}
  1. 首先进行加锁操作,保证线程安全;
  2. 调用 getg() 方法获取当前 goroutine 的指针,用于绑定给一个 sudog
  3. 调用 acquireSudog() 方法获取一个 sudog,可能是新建的 sudog,也有可能是从缓存中获取的。设置好 sudog 要发送的数据和状态。比如发送的 Channel、是否在 select 中和待发送数据的内存地址等等。
  4. 完成指针指向,建立 sudoggoroutinechannel 之间的指向关系;把 sudog 添加到当前 channel阻塞写协程队列中
  5. 调用 gopark 方法挂起当前 goroutine,状态为 waitReasonChanSend,阻塞等待 channel。
  6. 倘若协程从 park 中被唤醒,则回收 sudog(sudog能被唤醒,其对应的元素必然已经被读协程取走);
  7. 解锁,返回

写流程整体串联

在这里插入图片描述

小结

关于 channel 发送的源码实现已经分析完了,针对 channel 各个状态做一个小结。

Channel StatusResult
Writenil阻塞
Write打开但填满阻塞
Write打开但未满成功写入值
Write关闭panic
Write只读Compile Error

channel 发送过程中包含 2 次有关 goroutine 调度过程:

  • 当接收队列中存在 sudog 可以直接发送数据时,执行 goready()将 g 插入 runnext 插槽中,状态从 Gwaiting 或者 Gscanwaiting 改变成 Grunnable,等待下次调度便立即运行。
  • 当 channel 阻塞时,执行 gopark() 将 g 阻塞,让出 cpu 的使用权。

需要强调的是,通道并不提供跨 goroutine 的数据访问保护机制。如果通过通道传输数据的一份副本,那么每个 goroutine 都持有一份副本,各自对自己的副本做修改是安全的。当传输的是指向数据的指针时,如果读和写是由不同的 goroutine 完成的,那么每个 goroutine 依旧需要额外的同步操作。


读流程

异常 case1:读空 channel

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {if c == nil {gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)throw("unreachable")}// ...
}
  • park 挂起,引起死锁,报错异常;

异常 case2:channel 已关闭且内部无元素

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {lock(&c.lock)if c.closed != 0 {if c.qcount == 0 {unlock(&c.lock)if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}// The channel has been closed, but the channel's buffer have data.} // ...
}

如果 channel 已经关闭且不存在缓存数据了,则清理 ep 指针中的数据并返回。这里也是从已经关闭的 channel 中读数据,读出来的是该类型零值的原因。

读流程

异常 case1:读空 channel

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {if c == nil {gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)throw("unreachable")}// ...
}
  • park 挂起,引起死锁,报错异常;

异常 case2:channel 已关闭且内部无元素

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {lock(&c.lock)if c.closed != 0 {if c.qcount == 0 {unlock(&c.lock)if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}// The channel has been closed, but the channel's buffer have data.} // ...
}

如果 channel 已经关闭且不存在缓存数据了,则清理 ep 指针中的数据并返回。这里也是从已经关闭的 channel 中读数据,读出来的是该类型零值的原因。

case3:读时有阻塞的写协程(同步接收)

在这里插入图片描述

前提:在 channel 的发送队列中找到了等待发送的 goroutine。取出队头等待的 goroutine。

  • 如果缓冲区的大小为 0,则直接从发送方接收值
  • 否则,对应缓冲区满的情况,从队列的头部接收数据,发送者的值添加到队列的末尾(此时队列已满,因此两者都映射到缓冲区中的同一个下标)。

也就是问题在go channel中,对于有缓冲的channel,如果channel满了,且有阻塞的写协程,此时有一个读协程,读取数据,那么流程是什么?

以下是具体的流程:

  1. 有一个有缓冲的 channel,其缓冲区已满。
  2. 一个写协程尝试向这个 channel 写数据,但因为 channel已满,所以写协程被阻塞。
  3. 同时,有一个读协程尝试从这个 channel 读数据。读协程成功读取到一个数据项,这将在 channel的缓冲区中腾出一个空位。
  4. 被阻塞的写协程立刻被唤醒,将数据写入刚刚腾出的空位。
  5. 读协程和写协程继续执行它们剩下的操作。

同步接收的核心逻辑见下面 recv() 函数:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {lock(&c.lock)// Just found waiting sender with not closed.if sg := c.sendq.dequeue(); sg != nil {recv(c, sg, ep, func() { unlock(&c.lock) }, 3)return true, true}
}func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if c.dataqsiz == 0 {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.qp := chanbuf(c, c.recvx)if 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}sg.elem = nilgp := sg.gunlockf()gp.param = unsafe.Pointer(sg)sg.success = truegoready(gp, skip+1)
}

需要注意的是由于有发送者在等待,所以如果存在缓冲区,那么缓冲区一定是满的。这个情况对应发送阶段阻塞发送的情况,如果缓冲区还有空位,发送的数据直接放入缓冲区,只有当缓冲区满了,才会打包成 sudog,插入到 sendq 队列中等待调度。注意理解这一情况。

接收时主要分为 2 种情况,有缓冲且 buf 满和无缓冲的情况:

  • 无缓冲。ep 发送数据不为 nil,调用 recvDirect() 将发送队列中 sudog 存储的 ep 数据直接拷贝到接收者的内存地址中。
  • 有缓冲并且 buf 满。有 2 次 copy 操作,先将队列中 recvx 索引下标的数据拷贝到接收方的内存地址,再将发送队列头的数据拷贝到缓冲区中,释放一个 sudog 阻塞的 goroutine。[备注:缓冲区满了,可以直接读,但是读完需要善后,也就是把阻塞写协程的值拷贝过来,然后释放]。

case4:读时无阻塞写协程且缓冲区有元素(异步接收)

前提:如果 Channel 的缓冲区中包含一些数据时,但没有满,从 Channel 中接收数据会直接从缓冲区中 recvx 的索引位置中取出数据进行处理:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {lock(&c.lock)if c.qcount > 0 {// Receive directly from queueqp := chanbuf(c, c.recvx)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}
}
  1. 加锁;
  2. 获取到 recvx 对应位置的元素;
  3. recvx++;
  4. qcount–;
  5. 解锁,返回;

case5:读时无阻塞写协程且缓冲区无元素(阻塞接收)

前提:如果 channel 发送队列上没有待发送的 goroutine,并且缓冲区也没有数据时,将会进入到最后一个阶段阻塞接收:

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {lock(&c.lock)gp := getg()mysg := acquireSudog()mysg.elem = epgp.waiting = mysgmysg.g = gpmysg.c = cgp.param = nilc.recvq.enqueue(mysg)atomic.Store8(&gp.parkingOnChan, 1)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)gp.waiting = nilsuccess := mysg.successgp.param = nilmysg.c = nilreleaseSudog(mysg)return true, success
}
  • 调用 getg() 方法获取当前 goroutine 的指针,用于绑定给一个 sudog。
  • 调用 acquireSudog() 方法获取一个 sudog,可能是新建的 sudog,也有可能是从缓存中获取的。设置好 sudog 要发送的数据和状态。比如发送的 Channel、是否在 select 中和待发送数据的内存地址等等。
  • 调用 c.recvq.enqueue 方法将配置好的 sudog 加入待发送的等待队列。
  • 设置原子信号。当栈要 shrink 收缩时,这个标记代表当前 goroutine 还 parking 停在某个 channel 中。在 g 状态变更与设置 activeStackChans 状态这两个时间点之间的时间窗口进行栈 shrink 收缩是不安全的,所以需要设置这个原子信号。
  • 调用 gopark 方法挂起当前 goroutine,状态为 waitReasonChanReceive,阻塞等待 channel。

在这里插入图片描述

读流程和整体串联

在这里插入图片描述

状态表

操作nil的channel正常channel已关闭channel
<- ch(读)阻塞成功或阻塞有数据,正常接收
无数据,零值
ch <-(发)阻塞成功或阻塞panic
close(ch)panic成功panic

往 nil channel 上进行操作:

  • 发送:如果你尝试发送数据到一个 nil channel 上,操作会被阻塞。
  • 接收:如果你尝试从一个 nil channel 接收数据,操作也会被阻塞。
  • 关闭:你不能关闭一个 nil channel,否则会触发 panic。
  1. 对已关闭的 channel 进行操作:
  • 发送:如果你尝试向一个已关闭的 channel 发送数据,会触发 panic。
  • 接收:你可以从一个已关闭的 channel 接收数据。如果 channel 中还有数据,你会正常接收到数据;如果 channel 为空,你会接收到该通道类型的零值。接收操作不会被阻塞。
  • 关闭:如果你尝试关闭一个已经关闭的 channel,会触发 panic。

参考

https://zhuanlan.zhihu.com/p/597232906

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

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

相关文章

【移植代码】matlab.engine报错、numpy+mkl安装、Qt platform plugin报错总结

文章目录 numpy报错numpy安装PyQt5报错matlab.engine无法加载确认配置版本进行配置 matlab文件路径缺失vscode无法debug3.7以下版本总结 今天的任务是复现师姐的代码&#xff0c;代码在服务器的环境下可以跑&#xff0c;而我要做的&#xff0c;就是将环境和源码配置好&#xff…

JavaScript使用类-模态窗口

**上节课我们为这个项目获取了一些DOM元素&#xff0c;现在我们可以继续&#xff1b;**这个模态窗口有一个hidden类&#xff0c;这个类上文我们讲了&#xff0c;他的display为none&#xff1b;如果我们去除这个hidden的话&#xff0c;就可以让这个模态窗口展现出来。如下 cons…

yolov5加关键点回归

文章目录 一、数据1&#xff09;数据准备2&#xff09;标注文件说明 二、基于yolov5-face 修改自己的yolov5加关键点回归1、dataloader,py2、augmentations.py3、loss.py4、yolo.py 一、数据 1&#xff09;数据准备 1、手动创建文件夹: yolov5-face-master/data/widerface/tr…

【postgresql】

看到group by 1&#xff0c;2 和 order by 1&#xff0c; 2。看不懂&#xff0c;google&#xff0c;搜到了Stack Overflow 上有回答 What does SQL clause “GROUP BY 1” mean? 大概意思就是&#xff0c;group by&#xff0c; order by 后面跟数字&#xff0c;指的是 selec…

git命令笔记

git命令笔记 前言&#xff1a;git对于软件开发和协作的重要性不言而喻&#xff0c;在企业开发中&#xff0c;git命令和linux命令的使用同样重要。作为开发者&#xff0c;需要牢记并熟练使用常见的git命令 git工作流程图 命令如下&#xff1a; clone&#xff08;克隆&#xf…

虹科方案|国庆出游季,古建筑振动监测让历史古迹不再受损

全文导读&#xff1a; 国庆长假即将到来&#xff0c;各位小伙伴是不是都做好了出游计划呢&#xff1f;今年中秋、国庆“双节”连休八天&#xff0c;多地预计游客接待量将创下新高&#xff0c;而各地的名胜古迹更是人流爆满。迎接游客的同时&#xff0c;如何保障历史古迹不因巨大…

ATFX汇市:美国9月CPI数据来袭,机构预期年率增速将继续回落

ATFX汇市&#xff1a;今日20:30&#xff0c;美国劳工部将公布9月未季调CPI年率增速&#xff0c;前值为3.7%&#xff0c;预期值3.6%&#xff1b;9月未季调核心CPI年率&#xff0c;同一时间公布&#xff0c;前值为4.3%&#xff0c;预期值4.1%。无论是名义CPI增速还是核心CPI增速&…

vue配置@路径

第一步&#xff1a;安装path&#xff0c;如果node_module文件夹中有path就不用安装了 安装命令&#xff1a;npm install path --save 第二步&#xff1a;在vue.config.js文件&#xff08;如果没有就新建&#xff09;中配置 const path require("path"); function …

el-data-picker限制日期可选范围

<el-date-pickerclass"date"v-model"date"type"date"change"dateChange"value-format"yyyy-MM-dd"format"yyyy-MM-dd"placeholder"选择日期":picker-options"datePickerOptions"></…

基于SpringBoot的大学城水电管理系统

目录 前言 一、技术栈 二、系统功能介绍 管理员模块的实现 领用设备管理 消耗设备管理 设备申请管理 状态汇报管理 用户模块的实现 设备申请 状态汇报 用户反馈 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛…

【Java 进阶篇】JavaScript 介绍及其发展史

JavaScript是一门广泛应用于Web开发的编程语言。它是一种高级的、解释性的脚本语言&#xff0c;主要用于改善用户与Web页面的互动体验。本篇博客将为你详细介绍JavaScript的基础知识、历史背景和它在Web开发中的重要作用。我们还将讨论JavaScript的发展史&#xff0c;从它的起源…

【Java 进阶篇】JavaScript二元运算符详解

JavaScript是一门多用途的编程语言&#xff0c;它支持各种运算符&#xff0c;包括二元运算符。二元运算符用于执行两个操作数之间的操作&#xff0c;这两个操作数通常是变量、值或表达式。在本篇博客中&#xff0c;我们将详细探讨JavaScript的二元运算符&#xff0c;包括它们的…

设计模式-状态模式

介绍 一个对象有状态变化每次状态变化都会触发一个逻辑不能总是用if else来控制 示例 交通信号灯不同颜色的变化 UML类图 传统UML类图 简化后的UML类图 代码演示 // 状态&#xff08;红灯、绿灯、黄灯&#xff09; class State {constructor(color) {this.color col…

A股风格因子看板 (2023.10 第04期)

该因子看板跟踪A股风格因子&#xff0c;该因子主要解释沪深两市的市场收益、刻画市场风格趋势的系列风格因子&#xff0c;用以分析市场风格切换、组合风格暴露等。 今日为该因子跟踪第04期&#xff0c;指数组合数据截止日2023-09-30&#xff0c;要点如下 近1年A股风格因子检验统…

一站式解决方案:Qt 跨平台开发灵活可靠

一站式解决方案&#xff1a;Qt 跨平台开发灵活可靠 Qt 是一种跨平台开发工具&#xff0c;为开发者提供了一站式解决方案。无论您的项目目标是 Windows、Linux、macOS、嵌入式系统还是移动平台&#xff0c;Qt 都能胜任。这种跨平台的特性不仅节省开支&#xff0c;还推动了战略的…

CTF Misc(3)流量分析基础以及原理

前言 流量分析在ctf比赛中也是常见的题目&#xff0c;参赛者通常会收到一个网络数据包的数据集&#xff0c;这些数据包记录了网络通信的内容和细节。参赛者的任务是通过分析这些数据包&#xff0c;识别出有用的信息&#xff0c;例如登录凭据、加密算法、漏洞利用等等 工具安装…

【SQL】MySQL中的索引,索引优化

索引是存储引擎用来快速查询记录的一种数据结构&#xff0c;按实现方式主要分为Hash索引和B树索引。 按功能划分&#xff0c;主要有以下几类 单列索引指的是对某一列单独建立索引&#xff0c;一张表中可以有多个单列索引 1. 单列索引 - 普通索引 创建索引&#xff08;关键字i…

基于SpringBoot的城镇保障性住房管理系统

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 房屋类型管理 房源信息管理 房源申请管理 住房分配 房源申请 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上…

Exposure Normalization and Compensation for Multiple-Exposure Correction 论文阅读笔记

这是CVPR2022的一篇曝光校正的文章&#xff0c;是中科大的。一作作者按同样的思路&#xff08;现有方法加一个自己设计的即插即用模块以提高性能的思路&#xff09;在CVPR2023也发了一篇文章&#xff0c;名字是Learning Sample Relationship for Exposure Correction。 文章的…

新闻软文稿件媒体发布怎么做?纯干货

新闻软文稿件需要投放在正确的媒体上&#xff0c;才能获得更好的宣传推广效果&#xff0c;新闻软文稿件媒体发布怎么做&#xff1f;今天伯乐网络传媒就来给大家讲解一下&#xff0c;纯干货&#xff0c;建议收藏起来慢慢看。 一、媒体选择与分析 1. 确定目标媒体 在进行新闻软…