Golang の深い理解: チャネル パイプライン

チャネルの使用状況

チャンネル宣言方法

  • chInt := make(chan int) // バッファなしチャネル バッファなしチャネル
  • chInt := make(chan int, 0) // バッファなしチャネル バッファなしチャネル
  • chInt := make(chan int, 2) // バッファリングされたチャネル バッファリングされたチャネル

チャンネルの基本的な使い方

  • ch <- x // チャネルはデータ x を受信します
  • x <- ch // チャネルはデータを送信し、x に割り当てます
  • <- ch // チャネルはデータを送信し、受信者を無視します

バッファなしチャネルを使用する場合、この時点でデータをバッファに詰め込むときにすぐにデータを受信できる場所が必要です。そうでないと、常にブロックされてしまいます。原則として、この時点ではバッファーにデータは存在せず (バッファリングなし)、チャネルへのデータ送信は直接送信、つまりスリープを待っているコルーチンに直接送信すると見なされます。

func main() {
    
    
    ch := make(chan string)
    // 阻塞
    ch <- "ping"
    <-ch
}

コルーチンを開始してデータを取得します。

func main() {
    
    
    ch := make(chan string)

    // 程序能通过
    go func() {
    
    
        <-ch
    }()

    ch<-"ping"
}

記憶とコミュニケーション

記憶を共有することでコミュニケーションするのではなく、コミュニケーションすることで記憶を共有するのです。主に:

  • コルーチンの競合とデータの衝突を避けてください。
  • より高いレベルの抽象化により、開発の困難さが軽減され、プログラムの可読性が向上します。
  • モジュールの分離が容易になり、拡張性と保守性が向上します。

共有メモリのケースを通じて:

func watch(p *int) {
    
    
    for {
    
    
        if *p == 1 {
    
    
            fmt.Println("go")
            break
        }
    }
}

func main() {
    
    
    i := 0
    go watch(&i)

    time.Sleep(time.Second)

    i = 1

    time.Sleep(time.Second)
}

通信方法は以下の通りです。

func watch(c chan int) {
    
    

    if <-c == 1 {
    
    
        fmt.Println("go")
    }
}

func main() {
    
    
    c := make(chan int)
    go watch(c)

    time.Sleep(time.Second)

    c <- 1

    time.Sleep(time.Second)
}

チャネル設計

ここに画像の説明を挿入

チャネルは Go の下部にある hchan 構造として表されます。

type hchan struct {
    
    
    /* 缓存区结构开始 */
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    elemtype *_type // element type
    /* 缓存区结构结束*/

    // 发送队列
    sendx    uint   // send index
    sendq    waitq  // list of send waiters

    // 接收队列
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters

    lock mutex
    // 0:关闭状态;1:开启状态
    closed   uint32
}

type waitq struct {
    
    
    first *sudog
    last  *sudog
}

データはリング バッファリング バッファーに保存され、メモリ/GC オーバーヘッドを削減できます。

c <- “x” の糖衣構文に関しては、Go のチャネル データ送信の原則により、
c <- が chansend1 メソッドにコンパイルされます。

// %GOROOT%src/runtime/chan.go
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
    
    
    chansend(c, elem, true, getcallerpc())
}

データを送信する 3 つの状況:

  1. バッファにデータが存在せず、チャネルへのデータ送信は直接送信とみなされ、受信待ちのコルーチンのアクセプタにデータが直接コピーされ、コルーチンが起動されます。受信待ちのコルーチンが存在しない場合は、データをバッファに入れます。
  2. バッファーにデータがあってもバッファーがいっぱいではない場合、データはバッファーに保管されます。
  3. 受信キューに休止中の待機コルーチンがなく、バッファーがいっぱいの場合、データは sudog にパッケージ化され、休止中の待機のために sendq キューに入れられ、その後チャネルによってロックが解除されます。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    
    
    // ...
    if c.closed != 0 {
    
    
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }
    // 1. 取出接收等待队列 recvq 的协程,将数据发送给它
    if sg := c.recvq.dequeue(); sg != nil {
    
    
        send(c, sg, ep, func() {
    
     unlock(&c.lock) }, 3)
        return true
    }

    // 接收等待队列中没有协程时
    if c.qcount < c.dataqsiz {
    
    
        // 2. 缓存空间还有余量,将数据放入缓冲区
        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
    }
    // ...

    // 3. 休眠等待
    gp := getg()
    // 包装为 sudog
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
    
    
        mysg.releasetime = -1
    }

    // 将数据,协程指针等记录到 mysq 中
    mysg.elem = ep
    mysg.waitlink = nil
    mysg.g = gp
    mysg.isSelect = false
    mysg.c = c
    gp.waiting = mysg
    gp.param = nil

    // 将 mysg 自己入队
    c.sendq.enqueue(mysg)

    // 休眠
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
    KeepAlive(ep)

    // 被唤醒后再维护一些数据,注意,此时的记录的数据已经被拿走
    if mysg != gp.waiting {
    
    
    throw("G waiting list is corrupted")
    }
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success
    gp.param = nil
    // ...
}

rec <-c の文法糖に関しては、チャネル データ受信原理
Go は、次のように <-c を func chanrecv メソッドにコンパイルします。

  • コンパイル段階では、rec <- c は次のように変換されます。runtime.chanrecv1()
  • コンパイル段階では、rec, ok <- c は次のように変換されます。runtime.chanrecv2()
  • chanrecv()最終的にメソッドを呼び出します
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
    
    
    chanrecv(c, elem, true)
}

//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    
    
    _, received = chanrecv(c, elem, true)
    return
}

データを受信する4つのケース:

  1. 待機中の送信コルーチン (sendq) がありますが、バッファは空で、コルーチンから受信しています。
  2. 待機中の送信コルーチン (sendq) がありますが、バッファは空ではなく、キャッシュから受信されています。
  3. 受信バッファ
  4. 受信をブロックする
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    
    
    // ...
    if c.closed != 0 {
    
    
        // ...
    } else {
    
    
        // 1.,2. 接收数据前,已经有协程在休眠等待发送数据
        if sg := c.sendq.dequeue(); sg != nil {
    
    
            // 在 1 的情况下,缓存为空,直接从 sendq 的协程取数据
            // 在 2 的情况下,从缓存(缓冲)取走数据后,将 sendq 里的等待中的协程的数据放入缓存,并唤醒该协程
            // 这就是为什么 sendq 队列中的协程被唤醒后,其携带的数据已经被取走的原因
            recv(c, sg, ep, func() {
    
     unlock(&c.lock) }, 3)
            return true, true
        }
    }

    // 3. 直接从缓存接收数据
    if c.qcount > 0 {
    
    
        qp := 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
    }

    // ...
    // 缓冲区为空,同时 sendq 里也没休眠的协程,则休眠等待
    gp := getg()
    mysg := acquireSudog()
    mysg.releasetime = 0
    if t0 != 0 {
    
    
        mysg.releasetime = -1
    }
    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)

    // 休眠
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

    // ...
}

チャネルからデータを受信するプロセス中にウェイクアップされ、以前にデータがなかったため休止状態で待機していたことを示します。送信者がデータを送信すると、そのデータを受信者のローカルにアクティブにコピーします。

おすすめ

転載: blog.csdn.net/by6671715/article/details/131484449