关于后端:Go源码解析之selectgo

53次阅读

共计 9063 个字符,预计需要花费 23 分钟才能阅读完成。

File: select.go

select.go 这个文件是 Go 语言运行时(runtime)中的一个模块,次要负责实现 Go 语言中的 select 语句。Select 语句是 Go 语言中一种用于解决多个通道并发操作的语句,能够让程序阻塞期待多个通道中的任意一个有可读取的数据或可写入的空间,从而实现对多个并发操作的对立管制。

在 select.go 文件中,次要蕴含了以下几个局部:

  1. select 构造体:用于定义 select 语句的构造体类型,蕴含了可读通道、可写通道、已执行状态等信息。
  2. selectcase 构造体:用于定义 select 语句中单个 case 子句的构造体类型,蕴含了通道、方向、发送 / 接管操作、已执行状态等信息。
  3. selectgo 函数:用于在新的 goroutine 中执行 select 语句。
  4. selectnbrecv、selectnbrecv2、selectnbsend、selectnbsend2 等函数:用于执行 select 语句中的接管 / 发送操作,并返回操作后果。
  5. selectsetpc 函数:用于将以后程序计数器(PC)设置为给定地位,从而跳转到对应的代码块中执行。

总的来说,select.go 文件的作用是实现了 Go 语言中的 select 语句,利用多通道并发操作的个性实现了高效的并发编程。在应用 select 语句时,编译器会主动调用 select.go 中的代码来实现语句的执行。


Var:

chansendpc

变量 chansendpc 定义在 select.go 文件中,用于示意 chansend 函数的程序计数器(Program Counter,PC),它记录了正在执行 chansend 函数时的代码地址。在 Golang 中,程序计数器是一个寄存器,它保留了以后正在执行的代码地址。

在执行 chansend 函数时,如果发送操作无奈立刻实现,即通道已满,则该 goroutine 会被阻塞,期待接收者从通道中读取数据。这时,该 goroutine 会被放到通道的发送者队列中,期待接收者读取数据后唤醒它。同时,执行 chansend 函数的程序计数器也会被保存起来,后续该 goroutine 被唤醒时会从保留的 PC 处继续执行。

通过应用程序计数器,Golang 能够实现 goroutine 的合作和调度,以实现高效的并发编程。

chanrecvpc

在 Go 语言的 runtime 包中,select.go 文件中的 chanrecvpc 变量的作用是保留和解决一个 Go 程序中应用的通道的 receive 操作。chanrecvpc 变量是一个构造体类型的变量,其定义如下:

type hchan struct {// ... 其余的字段}

type sudog struct {// ... 其余的字段}

type selectCtx struct {
    // ... 其余的字段
    recvOK    bool       // 接管操作是否胜利
    elem      unsafe.Pointer // 接管到的元素
    selected  *sudog     // 曾经被选中的 sudog
    order     uint32     // 选中的 sudog 在列表中的程序
    //
    // Select 操作阻塞期待时会调用 onWait 办法,该办法会将 block 操作携带的 chanrecvpc 协程放入到 sudog 期待队列中
    //
    // 一旦 chanrecvpc 协程被唤醒,会调用 onWakeup 办法,它会将 block 操作携带的 chanrecvpc 协程从 sudog 期待队列中删除
    onWait    func(sg *sudog) // 暂存 chanrecvpc 协程的阻塞办法
    onWakeup  func(sg *sudog) // 查问 chanrecvpc 协程是否被唤醒的办法
    orderChan chan uint32 // 从 sudog 告诉 chanrecvpc 排名信息的管道
}

// chanrecvpc 示意 "channel receive primitive with a PC",即带有 PC 值的通道接管原语
type chanrecvpc struct {
    ptr *uintptr // chan 存储地址
    chan_ interface{} // 期待接管的通道
    recvx *uintptr // 接管操作的值存储地址
    recvk uintptr // 接管操作的值存储大小
    done *uint32 // 接管操作状态值
    ncases int // 管道列表长度,即以后 select 语句中管道的数量
    cstmt uint32 // 该 chanrecvpc 在 select 语句列表中的编号
    list  *selectCtx // select 语句中所有通道的列表
    // ... 其余的字段
}

在 Go 语言中,通道是一种根本的数据类型,它能够实现不同的 goroutine 之间的同步和通信。chanrecvpc 变量的作用是在实现运行时的 Select 操作过程中,解决以后 select 语句中应用的通道接管操作(receive operation)的相干状态和内容。Select 操作能够同时期待多个通道的数据到来,并依据到来的数据进行不同的解决,示意一个 select 代码块的 chanrecvpc 用于解决一个通道接管操作的状态信息。同时,每个 chanrecvpc 对应一个 selectCtx 变量,该联合体变量用于保留 select 语句的相干状态,包含接管状态的输出信息和后果状态的输入信息。当这些输出或输入状态发生变化时,selectCtx 将用于告诉其余相关联的术语和变量,来执行正确的操作。因而,chanrecvpc 变量是实现 Select 操作的必要局部,因为它能够协调通道数据的接管以及 goroutine 和 channel 之间的交互。


Structs:

scase

scase 构造体示意一个 select 语句中的一个 case 子句,它记录了 case 子句中的操作、通道以及通道操作的后果等。

scase 构造体的定义如下:

type scase struct {c    *hchan             // Channel to use (may be nil)
    recv reflect.Value      // Argument to send/receive
    kind reflect.SelectCase // select{Recv,Send,Default}
    pc   uintptr            // return pc
}
  • c:示意要进行 channel 操作的通道,如果是 default 分支则为 nil。
  • recv:示意要发送或接管的值。如果是发送操作,则该字段必须是可寻址的,否则会抛出 panic。如果是接管操作,则该字段必须是指向贮存接管值的地址的指针,否则同样会抛出 panic。
  • kind:示意这个 case 分支所对应的类型,可选值有 reflect.SelectRecv(示意接管操作)、reflect.SelectSend(示意发送操作)和 reflect.SelectDefault(示意默认分支)。
  • pc:示意分支语句的返回地址,用于跳转。如果是 default 分支,则该字段为 0。

在 select 语句的执行过程中,零碎会为每一个 case 分支创立一个 scase 对象,并将分支所对应的数据赋值给它对应的 scase 对象。同时,这些 scase 对象将依照在 select 语句中呈现的顺序存储在 sel 中的 scase 数组中。之后,零碎通过执行一系列轮询和条件判断来确定应该抉择哪个分支。

总之,scase 构造体记录了 select 语句中的 case 子句所须要的全副信息,为 select 语句的运行提供了必要的反对。

runtimeSelect

在 Go 语言中,select 语句用于监听多个通道的读写工作,它能够期待多个通道中的第一个 I/O 操作实现并执行相应的操作。而 runtimeSelect 构造体就是用来反对 select 语句的。

runtimeSelect 构造体定义在 select.go 文件中,其中蕴含了一些字段和办法。上面是 runtimeSelect 构造体的定义:

type runtimeSelect struct {tcase       unsafe.Pointer // []scase
    pollorder   unsafe.Pointer // []uint16 用于轮询 scase 的程序
    lockorder   *uint16        // 多 goroutine select 时的锁和程序,用于防止饥饿
    ncase       uint16
    pollclosed  uint32
    waitclosed  uint32
    startTime   int64
    context     *sudog
    options     uint32
}

runtimeSelect 构造体的几个次要字段含意如下:

  • tcase:一个 scase 类型的切片,每个 scase 类型示意一个通道的读写操作;
  • pollorder:一个 uint16 类型的切片,用于存储 tcase 中通道的索引;
  • lockorder:一个指向 uint16 类型的指针,示意多个 goroutine 应用雷同的 select 时的锁和程序;
  • ncase:示意 tcase 中 scase 的数量;
  • pollclosed 和 waitclosed:示意 select 是否敞开;
  • context:一个指向 sudog 类型的指针,用于唤醒此 select 监听的 goroutine;
  • options:示意 select 的一些选项,例如是否设置了超时和抢占等。

除了上述字段之外,runtimeSelect 构造体也提供了一些办法,用于解决 select 语句的执行过程,例如 runtimeSelect.selectcase 和 runtimeSelect.block 等。

在 Go 的运行时中,当执行到 select 语句时,会创立一个 runtimeSelect 类型的构造体,并依据 select 中的 case 条件初始化 tcase 和 pollorder 等字段。接着,runtimeSelect.selectcase 办法会遍历 tcase 中所有的 scase,查看其中的操作是否能够执行。如果有操作能够执行,就会执行这个操作并返回后果。如果所有操作都无奈执行,则进入 runtimeSelect.block 办法期待某个操作能够执行。当有操作能够执行时,runtimeSelect.selectcase 办法就会解决相应的操作并返回。这样,就实现了 select 语句的执行。

selectDir

在 Go 语言中,select 语句用于从多个 channel 中接收数据,它会始终阻塞,直到其中一个 channel 能够进行数据交互为止。为了实现这一性能,Go 在 runtime 包中定义了 selectDir 构造体。

selectDir 构造体示意一个 channel 的数据传输方向,它有以下几个字段:

  • casep:指向 case 构造体的指针,示意 select 语句中的某个 case 语句。
  • dir:枚举类型,示意 channel 的传输方向,有发送 (send)、接管(recv) 和默认 (default) 三种。
  • index:对于发送操作,示意要发送的数据在 case 中的地位;对于接管操作,示意要接收数据的变量在 case 中的地位;对于默认操作,示意该 case 在所有 case 中的地位。

selectDir 构造体的作用次要体现在 selectnbrecv 和 selectnbsend 函数中。这两个函数会在 selectDir 中示意的 channel 上进行非阻塞的接管或发送操作。

总的来说,selectDir 构造体是 select 语句实现中的一个重要组成部分,用于示意 channel 的数据传输方向,并反对非阻塞的数据传输操作。

Functions:

selectsetpc

selectsetpc()函数是 Go 语言中 runtime 包中 select.go 文件中的一个函数,它的作用是设置 goroutine 的 PC 值和 SP 值,以此切换到抉择的 case 执行体中。

在 Go 语言中,select 语句用于在多个通道之间进行抉择。当一个或多个 channel 中呈现数据时,select 语句会选出其中一个 case 进行解决,而其余的 case 将被疏忽。

在实现 select 语句时,Go 语言应用了一种非凡的形式来实现并发的抉择。它首先会将所有 case 包装为一个 select-case 构造体,并存放到一个数组中,而后调用 selectgo()函数执行。

在 selectgo()函数中,会应用 selectsetpc()函数将下一个须要执行的 case 的 PC 值和 SP 值保留到以后的运行栈中,而后应用 Go 语言的栈切换机制,将以后的 goroutine 挂起,切换到下一个 goroutine 执行。

当下一个 goroutine 执行结束后,会将之前保留的 PC 值和 SP 值复原,继续执行 selectgo()函数,直到所有的 case 都被处理完毕或者等待时间超时。

总之,selectsetpc()函数的作用是在 Go 语言的 select 实现中,对 goroutine 的 PC 值和 SP 值进行设置,以此实现并发的抉择和切换。

sellock

在 Go 语言中,select语句用于在多个通道之间进行抉择操作。当有多个通道都处于可操作状态时,select语句会随机抉择一个通道进行操作,从而实现非阻塞的并发编程。

sellock函数是 select 语句中的一个要害函数,它用于对所有的通道进行加锁操作。sellock函数会先获取以后 g(goroutine)的selectdone 指针,而后遍历所有的通道,一一对其进行加锁操作。当某个通道被选中后,sellock函数会对其进行解锁操作,而后返回该通道的索引。

sellock函数的实现源码如下:

func sellock(sc *sudog, locked *bool) (selIdx uint16) {
    // ...
    // Get the g.selectDone (if not already cached).
    sdc := g.selectDone
    if sdc == nil {sdc = new(selectDone)
        g.selectDone = sdc
    }
    // ...
    // Lock all channels involved.
    sc.locked = locked
    sc.next = sdc.waitq
    sdc.waitq = sc
    lock(&sc.elem.getPtr().qlock)

    for i := uint16(0); i < sc.sel.n; i++ {if sc.sel.rcases[i].kind == recv {lock(&sc.sel.rcases[i].ch.recvq.lock)
            sc.sel.rcases[i].ch.recvq.waiting++
        } else {lock(&sc.sel.rcases[i].ch.sendq.lock)
            sc.sel.rcases[i].ch.sendq.waiting++
        }
    }
    // ...
    return selected
}

具体来说,sellock函数实现的外围逻辑包含:

  1. 获取以后 gselectdone指针,如果指针为 nil 则新建一个 selectDone 对象。
  2. sudog 对象的 locked 字段设置为locked
  3. sudog 对象退出到 selectdone 的期待队列中。
  4. 对所有的通道进行加锁操作,统计期待通道个数。
  5. 返回被选中的通道的索引。

selunlock

在 select 的实现中,selunlock()是用来解锁互斥锁 m 锁的。在执行 select 操作期间,多个 goroutine 可能被阻塞在不同的通道上,当其中一个通道筹备好后,就须要唤醒对应的 goroutine 进行解决。

selunlock()的次要作用是用来解锁互斥锁,容许在通道上期待的 goroutine 能够从新竞争互斥锁,以便抢占其余通道中的数据。

具体地说,当某个 goroutine 被阻塞在某个通道上调用 selunlock()后,会解除阻塞并将 goroutine 从 g 期待队列中删除。同时,该通道下的其余期待 goroutine 也会被唤醒并竞争互斥锁。如果没有其余期待 goroutine,则互斥锁就会被开释并返回。

总之,selunlock()函数能够协调多个 goroutine 在 select 操作期间对通道的竞争,解除阻塞并唤醒须要解决的 goroutine。

selparkcommit

selparkcommit 函数是用于在 select 操作中进行 park 的函数,并且在 park 时从新提交 M(goroutine 所在的线程)以回到 syscall。select 操作是一种用于在多个 channel 上期待数据的机制,而 selparkcommit 函数的作用就是将以后的 M 挂起,在期待期间由其余线程负责 M 的工作,当有数据可用时唤醒 M 并从新提交 M 以继续执行后续的操作。

具体来说,selparkcommit 函数首先获取以后 M 的 g 状态(也就是 goroutine 的状态),如果以后 g 的状态为 Grunning(正在运行),则将其状态批改为 Gwaiting(期待中)以便于在期待期间不被调度器选中,而后再调用 park 函数将以后 M 挂起。挂起之后,以后 M 将进入 waiting 状态,期待其余线程的唤醒。

当其余线程有数据可用时,它会通过 localWake(本地唤醒)或 globalWake(全局唤醒)函数唤醒以后 M,此时 selparkcommit 函数会从新将 g 的状态批改为 Grunnable(可运行)并将其提交以继续执行。如果在期待期间 select 操作被勾销或超时,selparkcommit 函数将间接从 park 中返回,以后 M 将不会进入 waiting 状态,而是立刻继续执行后续的操作。

总的来说,selparkcommit 函数的作用是在 select 操作中进行 park,挂起以后 M 并期待其余线程进行唤醒,以便于在期待期间由其余线程负责 M 的工作,从而进步了零碎的并发性能。

block

在 Go 语言中,select 语句用于从一组通道中检索一个能够进行通信的操作。没有任何操作能够进行时,select 语句将会阻塞,直到至多有一个通道能够进行通信。

在 runtime 包的 select.go 文件中,block 函数用于将 goroutine 阻塞在 select 语句中,直到至多有一个通道能够进行通信。

具体来说,block 函数会创立一个 SudoG 构造体,将其退出到 select 语句的阻塞列表中,并将该 goroutine 挂起。当至多有一个通道能够进行通信时,会从阻塞列表中移除 SudoG 并唤醒对应的 goroutine,使其继续执行。

此外,block 函数还会记录以后 goroutine 的 m 和 g,这些信息能够在唤醒时复原,使得复原后的 goroutine 能够继续执行之前的操作。

总之,block 函数的作用是将 goroutine 阻塞在 select 语句的阻塞列表中,直到至多有一个通道能够进行通信,并在失当的时候唤醒对应的 goroutine。

selectgo

selectgo 函数是 runtime 包中 select 语句的实现。它用于在多个通道之间抉择交互某些操作。在 Golang 中,select 语句是用于同时期待多个通道操作的高级控制结构。

selectgo 函数的次要作用是:

1. 治理 Goroutine 的抉择逻辑,使得 Goroutine 能够在多个通道中进行交互。
2. 用于实现 select 语句,因而被调用时它会期待所有与该语句关联的通道都有可用的数据或者产生读写事件。
3. 一旦某个通道有可用的数据或者产生读写事件,selectgo 函数会抉择这个通道并执行相应的操作。如果多个通道同时有可用的数据或者多个通道同时产生读写事件,则随机抉择一个通道并执行相应操作。

总的来说,selectgo 函数是 Golang 中十分重要的一个函数,它表演了多通道合作的枢纽角色,使得 Goroutine 能够更加高效地解决并发问题。

sortkey

在 Go 语言中,select 语句用于解决多个通道的数据。在 select.go 文件中,sortkey()函数用于对通道汇合进行排序,以确定哪个通道能够被抉择。该函数应用的策略是将通道汇合依照通道 ID 的程序进行排序,从而使较小的 ID 优先被抉择。

具体地说,sortkey()函数接管一个通道汇合作为参数,并返回一个排序后的通道汇合和一个值映射表。其中值映射表用于将排序后的通道索引映射回原始的通道索引,从而在后续解决中应用。该函数的实现应用了排序算法中的疾速排序(quicksort)。

总之,sortkey()函数在 select 语句的实现中施展了十分重要的作用,它通过排序通道汇合确定了哪个通道能够被抉择,从而保障了程序的正确性和稳定性。

reflect_rselect

在 Go 语言中,select 语句用于同时期待多个通道的操作。在 runtime 包的 select.go 文件中,reflect_rselect 函数是用于执行 select 语句的实现。当 select 语句中的 case 子句中有一个或多个通道时,reflect_rselect 函数会调用 reflect.chanrecv 函数对通道进行接管操作,并返回第一个筹备好的通道的操作后果。

reflect_rselect 函数的具体作用包含以下几点:

  1. 调用 reflect.chanrecv 函数执行通道的接管操作。
  2. 依据通道的操作后果执行相应的 case 子句。
  3. 如果所有的 case 子句都没有筹备好,则期待其中一个筹备好再执行相应的 case 子句。
  4. 如果所有的 case 子句都曾经筹备好,则随机抉择一个 case 子句执行相应的操作。

总体来说,reflect_rselect 函数是 select 语句的外围实现,它实现了期待多个通道的操作,对筹备好的通道进行操作,并依据后果执行相应的 case 子句。

dequeueSudoG

在 Go 语言中,select 语句能够同时期待多个 channel 的操作。当其中一个 channel 准备就绪,就会执行对应的 case 分支。dequeueSudoGselect 的一个实现办法,作用是从某个 channel 中取出可执行的 goroutine(协程)。

dequeueSudoG 会从一个 waitq 的队列里取出一个 goroutine,而后将其退出到 running 的队列里。具体流程为:

  1. 从队列头取出一个 sudog,代表一个期待着能够执行的 goroutine
  2. sudog 的状态设置为执行状态。
  3. sudog 增加到 running 的队列中。
  4. 返回 sudog

dequeueSudoG 中波及的 waitqrunning 队列是 runtime 包中用来治理协程的两个重要队列。其中,waitq 寄存期待 channelgoroutinerunning 寄存正在运行的 goroutine

总的来说,dequeueSudoG 的作用是从期待 channel 的队列中取出一个能够立刻执行的协程,并将其退出到运行队列中,以确保并发执行的正确性和高效性。

本文由 mdnice 多平台公布

正文完
 0