ホームページ > バックエンド開発 > Golang > Go チャンネルのロック解除: 仕組み

Go チャンネルのロック解除: 仕組み

Mary-Kate Olsen
リリース: 2025-01-17 02:11:10
オリジナル
357 人が閲覧しました

詳細な Golang チャネル: 実装原則とパフォーマンス最適化の提案

Golang のチャネルは、CSP 同時実行モデルの重要なコンポーネントであり、Goroutine 間の通信のブリッジです。 Channel は Golang で頻繁に使用されるため、その内部実装原理を深く理解することが重要です。この記事では、Go 1.13 ソース コードに基づいて Channel の基礎となる実装を分析します。

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

チャネルの実装を正式に分析する前に、その基本的な使用法を確認してみましょう:

<code class="language-go">package main
import "fmt"

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

    go func() {
        c <- 1 // 发送操作
    }()

    x := <-c // 接收操作
    fmt.Println(x)
}</code>
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このコードは、チャネルの 2 つの基本操作を示しています。

  • 送信操作: c <- 1
  • 受信操作: x := <-c

チャネルはバッファ付きチャネルとバッファなしチャネルに分かれています。上記のコードは、バッファリングされていないチャネルを使用しています。バッファリングされていないチャネルでは、現在データを受信して​​いる他の Goroutine が存在しない場合、送信側は send ステートメントでブロックされます。

チャネルを初期化するときにバッファ サイズを指定できます。たとえば、make(chan int, 2) はバッファ サイズを 2 に指定します。バッファがいっぱいになる前に、送信者は受信者の準備が整うのを待たずに、ブロックせずにデータを送信できます。ただし、バッファがいっぱいの場合でも、送信者はブロックします。

チャネルの基礎となる実装関数

チャネルのソース コードに入る前に、Golang でチャネルの具体的な実装場所を見つける必要があります。 Channel を使用すると、runtime.makechanruntime.chansendruntime.chanrecv などの基礎となる関数が実際に呼び出されます。

go tool compile -N -l -S hello.go コマンドを使用してコードをアセンブリ命令に変換することも、オンライン ツール Compiler Explorer (例: go.godbolt.org/z/3xw5Cj) を使用することもできます。組み立て説明書を分析すると、次のことがわかります。

  • make(chan int)runtime.makechan 関数に対応します。
  • c <- 1runtime.chansend 関数に対応します。
  • x := <-cruntime.chanrecv 関数に対応します。

これらの関数の実装は、Go ソース コードの runtime/chan.go ファイルにあります。

チャネル構造

make(chan int) はコンパイラによって runtime.makechan 関数に変換され、その関数シグネチャは次のとおりです:

<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このうち、t *chantype は Channel 要素の型、size int はユーザー指定のバッファ サイズ (指定しない場合は 0)、戻り値は *hchan です。 hchan は Golang のチャネルの内部実装構造であり、次のように定義されます。

<code class="language-go">type hchan struct {
        qcount   uint           // 缓冲区中已放入元素的数量
        dataqsiz uint           // 用户构造Channel时指定的缓冲区大小
        buf      unsafe.Pointer // 缓冲区
        elemsize uint16         // 缓冲区中每个元素的大小
        closed   uint32         // Channel是否关闭,==0表示未关闭
        elemtype *_type         // Channel元素的类型信息
        sendx    uint           // 缓冲区中发送元素的索引位置(发送索引)
        recvx    uint           // 缓冲区中接收元素的索引位置(接收索引)
        recvq    waitq          // 等待接收的Goroutine列表
        sendq    waitq          // 等待发送的Goroutine列表

        lock mutex
}</code>
ログイン後にコピー
ログイン後にコピー

の属性は、大きく 3 つのカテゴリに分類されます。 hchan

  • バッファ関連属性: bufdataqsizqcount など。チャネルのバッファ サイズが 0 以外の場合、バッファは受信するデータを格納するために使用され、リング バッファを使用して実装されます。
  • 待機キュー関連属性: recvq にはデータの受信を待機している Goroutine が含まれ、sendq にはデータの送信を待機している Goroutine が含まれます。 waitq二重リンクリストを使用して実装されます。
  • その他の属性: lockelemtypeclosed など。

makechan 関数は主に、バッファーや hchan などの属性の正当性チェックとメモリ割り当てを実行しますが、ここでは詳しく説明しません。

hchan 属性の簡単な分析に基づいて、バッファーと待機キューという 2 つの重要なコンポーネントがあることがわかります。 hchan のすべての動作と実装は、これら 2 つのコンポーネントを中心に展開します。

チャンネルデータ送信

チャンネルの送信プロセスと受信プロセスは非常に似ています。まず、チャネル (例: c <- 1) の送信プロセスを分析します。

が Channel にデータを送信しようとしたとき、recvq キューが空でない場合は、データの受信を待機している Goroutine が recvq ヘッダーから取り出され、データが直接 Goroutine に送信されます。コードは次のとおりです:

<code class="language-go">package main
import "fmt"

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

    go func() {
        c <- 1 // 发送操作
    }()

    x := <-c // 接收操作
    fmt.Println(x)
}</code>
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

recvq データの受信を待機している Goroutine が含まれています。 Goroutine が受信操作 (x := <-c など) を使用するとき、この時点で sendq が空でない場合は、Goroutine が sendq から取得され、データがそれに送信されます。

recvq が空の場合は、現時点でデータの受信を待機しているゴルーチンが存在しないことを意味し、チャネルはデータをバッファーに入れようとします:

<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このコードの機能は非常に単純で、データをバッファに入れることです。このプロセスにはリング バッファの操作が含まれます。dataqsiz はユーザー指定のバッファ サイズを表します (指定しない場合、デフォルトは 0)。

バッファリングされていないチャネルが使用されている場合、またはバッファがいっぱいである (c.qcount == c.dataqsiz) 場合、送信されるデータと現在のゴルーチンは sudog オブジェクトにパッケージ化され、sendq に配置され、現在のゴルーチンはGoroutine は待機するように設定されます ステータス:

<code class="language-go">type hchan struct {
        qcount   uint           // 缓冲区中已放入元素的数量
        dataqsiz uint           // 用户构造Channel时指定的缓冲区大小
        buf      unsafe.Pointer // 缓冲区
        elemsize uint16         // 缓冲区中每个元素的大小
        closed   uint32         // Channel是否关闭,==0表示未关闭
        elemtype *_type         // Channel元素的类型信息
        sendx    uint           // 缓冲区中发送元素的索引位置(发送索引)
        recvx    uint           // 缓冲区中接收元素的索引位置(接收索引)
        recvq    waitq          // 等待接收的Goroutine列表
        sendq    waitq          // 等待发送的Goroutine列表

        lock mutex
}</code>
ログイン後にコピー
ログイン後にコピー

goparkunlock は入力ミューテックスのロックを解除し、現在の Goroutine を一時停止して待機状態に設定します。 goparkgoready はペアで表示され、相互演算です。

ユーザーの観点から見ると、gopark を呼び出した後、データを送信するコード ステートメントがブロックされます。

チャンネルデータ受信

チャンネルの受信プロセスは基本的に送信プロセスと似ているため、ここでは詳しく説明しません。受信処理におけるバッファに関する動作の詳細については後述する。

Channel の送受信プロセス全体が runtime.mutex を使用してロックされることに注意してください。 runtime.mutex は、ランタイム関連のソース コードで一般的に使用される軽量ロックです。プロセス全体が最も効率的なロックフリーのソリューションではありません。 Golang のロックフリー チャネルに関する問題があります: go/issues#8899。

チャネルリングバッファの実装

チャネルはリング バッファを使用して、書き込まれたデータをキャッシュします。リング バッファには多くの利点があり、固定長 FIFO キューの実装に最適です。

Channel でのリング バッファの実装は次のとおりです:

hchan には、recvxsendx という 2 つのバッファー関連変数があります。 sendx はバッファ内の書き込み可能なインデックスを表し、recvx はバッファ内の読み取り可能なインデックスを表します。 recvxsendx の間の要素は、通常どおりバッファに入れられたデータを表します。

Go Channel Unlocked: How They Work

buf[recvx] を直接使用してキューの最初の要素を読み取り、buf[sendx] = x を使用して要素をキューの最後に置くことができます。

バッファ書き込み

バッファがいっぱいでない場合、バッファにデータを入れる操作は次のとおりです:

<code class="language-go">package main
import "fmt"

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

    go func() {
        c <- 1 // 发送操作
    }()

    x := <-c // 接收操作
    fmt.Println(x)
}</code>
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

chanbuf(c, c.sendx)c.buf[c.sendx] と同等です。上記のプロセスは非常に簡単で、データをバッファーの場所 sendx にコピーするだけです。

次に、sendxを次の位置に移動します。 sendx が最後の位置に到達すると、0 に設定されます。これは、一般的なエンドツーエンドのアプローチです。

バッファ読み取り

バッファがいっぱいでない場合、sendq も空でなければなりません (バッファがいっぱいでない場合、データを送信するゴルーチンはキューに入れられず、データを直接バッファに入れるためです)。このとき、チャネル chanrecv の読み取りロジックは比較的単純で、データをバッファから直接読み取ることもできます。これは、基本的に上記のバッファ書き込みと同じです。 recvx

内に待機中のGoroutineがある場合、この時点でバッファはフルになっている必要があります。このときのChannelの読み込みロジックは以下の通りです: sendq

<code class="language-go">func makechan(t *chantype, size int) *hchan</code>
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

は、データを受け取る変数に対応するアドレスです (たとえば、ep の場合、x := <-cep のアドレスです)。 x は、sg から取得された最初の sendq を表します。コード内: sudog

  • は、バッファー内の現在読み取り可能な要素を受け取り側の変数のアドレスにコピーすることを意味します。 typedmemmove(c.elemtype, ep, qp)
  • は、typedmemmove(c.elemtype, qp, sg.elem)のGoroutineによって送信されるのを待っているデータをバッファにコピーすることを意味します。 sendq は後で実行されるため、キューの最後にある recv にデータを配置することと同じになります。 sendq
簡単に言えば、ここでチャネルはバッファ内の最初のデータを対応する受信変数にコピーし、同時に

内の要素をキューの最後にコピーすることで、FIFO (先入れ先出し) を実装します。 。 sendq

概要

チャネルは、Golang で最もよく使用される機能の 1 つです。そのソース コードを理解することは、チャネルをよりよく使用し、理解するのに役立ちます。同時に、過度に迷信的になって Channel のパフォーマンスに依存しないでください。Channel の現在の設計には、最適化の余地がまだたくさんあります。

最適化の提案:

  • パフォーマンスを向上させるには、より軽量なロック メカニズムまたはロックフリー スキームを使用します。
  • バッファ管理を最適化し、メモリ割り当てとコピー操作を削減します。

Leapcell: Golang Web アプリケーションに最適なサーバーレス プラットフォーム

Go Channel Unlocked: How They Work

最後に、Go サービスのデプロイに非常に適したプラットフォームをお勧めします: Leapcell

  1. 多言語サポート: JavaScript、Python、Go、または Rust の開発をサポートします。
  2. 無制限のプロジェクトを無料で展開: 使用した分だけ支払い、リクエストや手数料はかかりません。
  3. 非常にコスト効率が高い: 従量課金制で、アイドル料金はかかりません。例: 25 ドルは、平均応答時間 60 ミリ秒で 694 万件のリクエストをサポートします。
  4. スムーズな開発者エクスペリエンス: 簡単なセットアップのための直感的な UI、完全に自動化された CI/CD パイプラインと GitOps の統合により、実用的な洞察が得られます。
  5. 簡単なスケーラビリティと高いパフォーマンス: 自動的にスケーリングして高い同時実行性を簡単に処理し、運用上のオーバーヘッドをゼロにし、構築に集中します。
Go Channel Unlocked: How They Work

詳細については、ドキュメントを確認してください。

リープセル Twitter: https://www.php.cn/link/7884effb9452a6d7a7a79499ef854afd

以上がGo チャンネルのロック解除: 仕組みの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート