Goでのチャネルの配置方法

Goでのチャネルの配置方法に関する有益な記事「Golang:channels implementation」の翻訳。







Goはますます人気が高まっており、その理由の1つは競合プログラミングに対する優れたサポートです。 チャネルとゴルーチンは、競争力のあるプログラムの開発を大幅に簡素化します。 Goでのさまざまなデータ構造の実装方法に関する良い記事( スライスマップインターフェイスなど)がいくつかありますが、チャネルの内部実装についてはかなり書かれています。 この記事では、チャネルがどのように機能し、どのように内部で実装されるかを学びます。 (Goでフィードを使用したことがない場合は、 最初にこの記事を読むことをお勧めします。)







チャンネルデバイス



チャネル構造を解析することから始めましょう:















一般に、ゴルーチンは、非ブロッキング呼び出しのロックフリーチェックの場合を除き、チャネルでアクションを実行するときにミューテックスをキャプチャします(これについては後で詳しく説明します)。 Closedは、チャネルが閉じている場合は1に、閉じていない場合は0に設定されるフラグです。 これらのフィールドは、わかりやすくするために全体像からさらに除外されます。







チャネルは、同期(非バッファー)または非同期(バッファー)にできます。 まず、同期チャネルがどのように機能するかを見てみましょう。







同期チャンネル



次のコードがあるとしましょう:







package main func main() { ch := make(chan bool) go func() { ch <- true }() <-ch }
      
      





最初に、新しいチャネルが作成され、次のようになります。













Goは同期チャネルにバッファーを割り当てないため、バッファーポインターはnilで、 dataqsiz



はゼロです。 上記のコードは、最初のことが起こることを保証していません-チャンネルからの読み取りまたは書き込み、最初のアクションがチャンネルからの読み取りであると仮定します(記録が最初に開始されるときの逆の例については、バッファ付きチャンネルの例で後述します) 最初に、現在のゴルーチンは、チャネルが閉じられているかどうか、バッファリングされているかどうか、ゴルチンが送信キューにあるかどうかなど、いくつかのチェックを実行します。 この例では、チャネルには送信待ちのバッファもゴルーチンもありません。そのため、goroutinは自分自身をrecvq



とブロックに追加します。 このステップでは、チャネルは次のようになります。













現在、チャネルにデータを書き込もうとしている作業ゴルーチンは1つだけです。 すべてのチェックが再び繰り返され、ゴルーチンがrecvq



キューをチェックすると、 recvq



が読み取りを待機していることを検出し、キューから削除し、スタックにデータを書き込み、ロックを解除します。 これは、あるGoroutineが別のGoroutineのスタックに直接書き込むときのGoランタイム全体の唯一の場所です。 このステップの後、チャネルは初期化直後とまったく同じに見えます。 両方のゴルーチンが終了し、プログラムが終了します。







これは、同期チャネルの配置です。 それでは、バッファリングされたチャンネルを見てみましょう。







バッファリングされたチャンネル



次の例を考えてみましょう。







 package main func main() { ch := make(chan bool, 1) ch <- true go func() { <-ch }() ch <- true }
      
      





繰り返しますが、実行の順序は不明です。最初に調べた最初の読み取りゴルーチンの例では、2つの値がチャネルに記録され、その後要素の1つが減算されたと仮定します。 そして最初のステップは、次のようなチャネルを作成することです。













同期チャネルとの違いは、Goがバッファを選択し、 dataqsiz



値を1に設定するdataqsiz



です。







次のステップは、最初の値をチャネルに送信することです。 これを行うために、ゴルーチンは最初にいくつかのチェックを行います: recvq



が空かどうか、バッファーが空かどうか、十分なバッファースペースがあるかどうか。







私たちの場合、バッファに十分なスペースがあり、読み取り用のキューにはゴルーチンがありません。そのため、ゴルーチンは単純に要素をバッファに書き込み、 qcount



値を増やして実行を継続します。 この時点でのチャネルは次のようになります。













次のステップで、goroutine mainは次の値をチャネルに送信します。 バッファがいっぱいになると、バッファ付きチャネルは同期(バッファなし)チャネルとまったく同じように動作します。つまり、goroutineは待機キューとブロックに自分自身を追加します。その結果、チャネルは次のようになります。













これでgoroutine mainがブロックされ、Goはチャネルから値を読み取ろうとする匿名のgoroutineを1つ起動しました。 そして、ここからトリッキーな部分が始まります。 Goは、チャネルがFIFOキュー( 仕様 )に基づいて機能することを保証しますが、ゴルーチンはバッファーから値を取得して実行を継続することはできません。 この場合、goroutine mainは永久にブロックされます。 この状況を解決するために、現在のゴルーチンはバッファからデータを読み取り、ブロックされたゴルーチンの値をバッファに追加し、待機中のゴルーチンのロックを解除して待機キューから削除します。 (ゴルーチンを待機している人がいない場合は、バッファから最初の値を読み取ります)







選択してください



ただし、Goは既定の動作で選択をサポートしています。また、チャネルがブロックされている場合、goroutinはどのように既定を処理できますか? いい質問です。プライベートチャネルAPIを簡単に見てみましょう。 次のコードを実行すると:







  select { case <-ch: foo() default: bar() }
      
      





Goは、次のシグネチャで関数を実行します。







 func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool)
      
      





chantype



はチャネルのタイプ(たとえば、make(chan bool)の場合はbool)、 hchan



はチャネル構造へのポインタ、 ep



はチャネルからのデータが書き込まれるメモリセグメントへのポインタです。 block



false



に設定されている場合、関数は非ブロックモードで動作します。 このモードでは、ゴルーチンはバッファとキューをチェックし、 true



を返してep



データを書き込むか、バッファにデータがないかキューに送信者がいない場合にfalse



返します。 バッファおよびキューのチェックはアトミック操作として実装され、ミューテックスをロックする必要はありません。







同様の署名を持つキューにデータを書き込むための関数もあります。







チャンネルからの録音と読み取りの仕組みを理解したので、チャンネルが閉じたときに何が起きるか見てみましょう。







チャンネル閉鎖



チャンネルを閉じるのは簡単な操作です。 Goは読み取りまたは書き込みを待機しているすべてのgoroutineを通過し、ロックを解除します。 すべての受信者は、そのタイプのチャネルデータの変数のデフォルト値を受け取り、すべての送信者はパニックします。







おわりに



この記事では、チャネルがどのように実装され、どのように機能するかを見ました。 できるだけシンプルに説明しようとしたので、詳細をいくつか見逃しました。 この記事の目的は、チャネルの内部構造の基本的な理解を提供し、より深い理解が必要な場合はGoソースコードを読むことをお勧めすることです。 チャネル実装コードを読んでください。 私には非常にシンプルで、十分に文書化されており、かなり短く、約700行のコードしかありません。







参照資料



ソースコード

Go仕様のチャンネル

ステロイド囲Channelチャンネル








All Articles