golang channel执行原理与代码分析

使用的go版本为 go1.21.2

首先我们写一个简单的chan调度代码

package main

import "fmt"

func main() {
	ch := make(chan struct{})

	go func() {
		ch <- struct{}{}
		ch <- struct{}{}
	}()

	fmt.Println("xiaochuan", <-ch)

	data, ok := <-ch
	fmt.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 *sudog
    last  *sudog
}

type sudog struct {
    // 下面的字段由 sudog 阻塞的 channel 的 hchan.lock 保护。
    // shrinkstack 依赖这个字段来处理参与 channel 操作的 sudog。

    g *g

    next *sudog
    prev *sudog
    elem unsafe.Pointer // 数据元素(可能指向堆栈)

    // 下面的字段在任何情况下都不会并发访问。
    // 对于 channels,waitlink 只有 g 访问。
    // 对于 semaphores,所有字段(包括上面的字段)
    // 仅在持有 semaRoot 锁时才会访问。

    acquiretime int64
    releasetime int64
    ticket      uint32

    // isSelect 表示 g 参与了 select,因此 g.selectDone 必须进行 CAS 操作以赢得唤醒竞争。
    isSelect bool

    // success 表示通信是否成功。如果 goroutine 被唤醒是因为在通道 c 上传递了值,则为 true,
    // 如果是因为 c 被关闭而唤醒,则为 false。
    success bool

    parent   *sudog // semaRoot 二叉树
    waitlink *sudog // g.waiting 列表或 semaRoot
    waittail *sudog // semaRoot
    c        *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"))
    }

    //初始化hchan
    var c *hchan
    switch {
    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 = elem
    c.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 int64
    if 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复制给qp
        typedmemmove(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 = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }

    //将sudog写入send环形队列中去
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil
    c.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 = nil
    gp.activeStackChans = false
    closed := !mysg.success
    gp.param = nil //清空G的参数列表
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    mysg.c = nil
    releaseSudog(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:295

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
        }
    }
    // 检测数据是否为空
    // 如果不为空直接调用sendDirect函数发送数据,然后将其重置为nil
    if sg.elem != nil {
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    //获取等待列表中的G,
    //将当前的ch解锁, sugo赋值为G当做启动参数
    gp := sg.g
    unlockf()
    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 int64
    if 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复制给ep
            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
    }

    
    gp := getg()//获取当前G
    //获取一个sudog, 优先从P中获取
    //如果P中的sudog缓存区(本地无锁)为空
    //从调度器层的sudog缓冲区(全局需要加锁)中拿数据放入P的sudog缓存区
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
        mysg.releasetime = -1
    }

    //将sudog写入recvq环形队列中去
    mysg.elem = ep
    mysg.waitlink = nil
    gp.waiting = mysg
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.param = nil
    c.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 = nil
    gp.activeStackChans = false
    if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
    }
    success := mysg.success
    gp.param = nil
    mysg.c = nil
    releaseSudog(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 sender
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
        // 获取环形队列中的元素
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            racenotify(c, c.recvx, nil)
            racenotify(c, c.recvx, sg)
        }
        // 如果数据不为空 直接ep复制给qp
        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
    }
    //获取等待列表中的G,
    //将当前的ch解锁, sugo赋值为G当做启动参数
    sg.elem = nil
    gp := sg.g
    unlockf()
    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:358

func 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 readers
    for { 
        //逐步从读取队列取值,直到获取完为止
        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.g
        gp.param = unsafe.Pointer(sg)
        sg.success = false
        if 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.g
        gp.param = unsafe.Pointer(sg)
        sg.success = false
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }
    unlock(&c.lock)

    // 循环glist待唤醒列表将G设置为read状态(唤醒G运行干活)
    for !glist.empty() {
        gp := glist.pop()
        gp.schedlink = 0
        goready(gp, 3)
    }
}

总结

我们从上面的源码分析了解chan的数据结构、发送数据、接收数据和关闭这些基本操作,从源码分析我们得知chan的读写操作是会上锁的,如果业务中对性能要求比较高的情况下chan的这把锁会成为我们系统内的瓶颈。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Xiamen I Fitness Platform

厦门I健身平台程 https://ijs.sports.xm.gov.cn/mgh5/#/ 1&#xff09;公众号 2&#xff09;主页 3&#xff09;【个人中心】【我的保险】就是要买一份保险&#xff0c;10元的那种&#xff0c;不然去场地出意外咋办 4&#xff09;我的保险状态&#xff1a;未购买&#xff0c;…

VR虚拟教育展厅,为教学领域开启创新之路

线上虚拟展厅是一项全新的展示技术&#xff0c;可以为参展者带来不一样的观展体验。传统的实体展览存在着空间限制、时间限制以及高昂的成本&#xff0c;因此对于教育领域来说&#xff0c;线上虚拟教育展厅的出现&#xff0c;可以对传统教育方式带来改革&#xff0c;凭借强大的…

【Qt之QSqlRelationalDelegate】描述及使用

描述 QSqlRelationalDelegate类提供了一个委托&#xff0c;用于显示和编辑来自QSqlRelationalTableModel的数据。 与默认委托不同&#xff0c;QSqlRelationalDelegate为作为其他表的外键的字段提供了一个组合框。 要使用该类&#xff0c;只需在带有QSqlRelationalDelegate实例…

easyrecovery如何恢复手机数据及硬盘数据恢复方法

EasyRecovery16是一款优秀的数据恢复软件&#xff0c;不仅能够兼容windows和mac双重系统&#xff0c;同时还能够识别u盘、存储卡、手机等多种数据储存设备&#xff0c;可恢复的文件类型更是多达百余种。还贴心地准备个人版、专业版和企业版的下载&#xff0c;增加了用户的可选性…

Android-P CameraSerivce

0 前言 本文重点分析Android-P的CameraService实现。 验证:Goldfish模拟器 1 定义 图1.1 CameraService ICameraServiceframeworks/av/camera/aidl/android/hardware/ICameraService.aidlBnCameraServiceout/soong/.intermediates/frameworks/av/camera/libcamera_client/an…

通过分析波形,透彻理解 UART 通信

UART是一种异步全双工串行通信协议&#xff0c;由 Tx 和 Rx 两根数据线组成&#xff0c;因为没有参考时钟信号&#xff0c;所以通信的双方必须约定串口波特率、数据位宽、奇偶校验位、停止位等配置参数&#xff0c;从而按照相同的速率进行通信。 异步通信以一个字符为传输单位…

远程访问:Windows设备管理器远程访问

设备管理器是一个应用程序&#xff0c;它包含与计算机耦合的硬件的完整概述&#xff0c;远程设备管理器允许管理员访问远程计算机的设备管理器&#xff0c;而无需远程访问桌面。 为什么需要远程设备管理器 IT环境充斥着数量众多的电脑和笔记本电脑&#xff0c;对于管理员来说…

Abaqus飞机起落架扭力臂拓扑优化

Abaqus飞机起落架扭力臂拓扑优化 Abaqus除了可以对结构进行强度分析&#xff0c;同样也自带强大的优化功能&#xff0c;下面通过一个简 单的实例演示在Abaqus中进行拓扑优化&#xff0c;另外&#xff0c;如果需要更加强大的拓扑优化仿真&#xff0c;可以 在TOSCA中进行。 定义接…

30岁的测试人?软件测试“内卷“?“我“该如何冲出破圈...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、软件测试的内卷…

怎么样的软件测试工程师才算“大神”?

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

数据集笔记 :PEMS-BAY

数据地址&#xff1a;DCRNN - Google 云端硬盘 各station 位置&#xff1a;DCRNN/data/sensor_graph/graph_sensor_locations_bay.csv at master liyaguang/DCRNN (github.com) 1 读取 数据 import h5py fileDownloads/pems-bay.h5fh5py.File(file,r) f.keys()f[speed] #&…

Linux系统iptables

目录 一. 防火墙简介 1. 防火墙定义 2. 防火墙分类 ①. 网络层防火墙 ②. 应用层防火墙 二. iptables 1. iptables定义 2. iptables组成 ①. 规则表 ②. 规则链 3. iptables格式 ①. 管理选项 ②. 匹配条件 ③. 控制类型 四. 案例说明 1. 查看规则表 2. 增加新…

事务基础知识

文章目录 1. 事务的ACID2. 事务隔离级别2.1 数据并发问题2.2 MySQL中的四种隔离级别 1. 事务的ACID 原子性&#xff08;atomicity&#xff09;&#xff1a; 原子性是指事务是一个不可分割的工作单位&#xff0c;要么全部提交&#xff0c;要么全部失败回滚。一致性&#xff08;…

专业的调查问卷平台推荐:提升数据收集与分析效率

无论是学生还是职场人士&#xff0c;想做好一份调查问卷&#xff0c;关键先要确定调查的主题&#xff0c;然后确定调查人群&#xff0c;编辑问题&#xff0c;最后能够尽可能的美化问卷调查的主题。 想要做到这几点&#xff0c;就要要求问卷调查平台: 1、能够帮助你快速制作出一…

初学者如何理解​session、cookie、token的区别与联系?

session、cookie、token。 相信学过接口的朋友都特别熟悉了。 但是对我一个刚接触接口测试的小白来说&#xff0c;属实有点分不清楚。 下文就是我通过查阅各种资料总结出来的一点理解&#xff0c;不准确的地方还请各位指正。 &#xff08;文末送洗浴中心流程指南&#xff09…

面试题:说一下MyBatis动态代理原理?

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1.MyBatis简介2.使用步骤2.1、引入依赖2.2、配置文件2.3、接口定义2.4、加载执行 3.原理解析 1.MyBatis简介 MyBatis是一个ORM工具&#xff0c;封装了JDBC的操作&a…

大数据机房迁移该按照什么步骤进行 |数据中心

前言&#xff1a; 机房搬迁不仅仅是把机房的设备迁移到新机房那么简单&#xff0c;而是要求网络系统的迁移和集中存储系统的迁移必须安全平稳&#xff0c;不能过长时间影响生产应用。表面上就是几个IT 民工的搬运&#xff0c;但实际是一项目高度集中的体力与脑力的综合项目。现…

SUKER书客领跑百亿台灯行业发展,用实力树立护眼台灯国货典范

随着近年人们生活水平不断提高&#xff0c;许多人经过疫情后也更加关注生活健康&#xff0c;护眼台灯市场规模也在进一步扩大。因为市场有着广阔的空间&#xff0c;为此吸引了不少企业品牌入局&#xff0c;这也导致行业在近年内野蛮生长&#xff0c;“产品质量不符”、“不符安…

燃料电池汽车市场分析:预计2028年将达到118亿美元

燃料电池汽车( FCV) 是一种用车载燃料电池装置产生的电力作为动力的汽车。车载燃料电池装置所使用的燃料为高纯度氢气或含氢燃料经重整所得到的高含氢重整气。与通常的电动汽车比较, 其动力方面的不同在于FCV 用的电力来自车载燃料电池装置, 电动汽车所用的电力来自由电网充电的…

教你用AI做艺术字,2个月,在小红书接广赚7200元

有段时间没给大家拆账号和完整地上教程了&#xff01;今天就来安排~如何用AI写艺术字&#xff0c;并且在小红薯实现商单BIANXIAN的系统教程.账号很多&#xff0c;我就拿这个AI艺术字搭配治愈系文案来展示下&#xff0c;这个比较有意思&#xff0c;艺术字治愈文案&#xff0c;视…