1.すべての例を実行: コードを読むだけではありません。入力して実行し、動作を観察してください。⚠️ このシリーズをどのように進めていきますか?
2.実験と破壊: スリープを削除して何が起こるか確認し、チャネル バッファー サイズを変更し、ゴルーチン数を変更します。
物を壊すことで、その仕組みがわかる
3.動作に関する理由: 変更されたコードを実行する前に、結果を予測してみてください。予期せぬ動作が見られた場合は、立ち止まってその理由を考えてください。解説に挑戦してください。
4.メンタル モデルの構築: 各ビジュアライゼーションはコンセプトを表します。変更されたコード用に独自の図を描いてみてください。
これは、「Mastering Go Concurrency」 シリーズの パート 1 であり、次の内容について説明します。
基本から始めて、それらを効果的に使用する方法についての直観を徐々に養っていきます。
少し長くなり、むしろ非常に長くなりますので、準備を整えてください。
私たちはプロセス全体を通して実践的にサポートします。
複数のファイルをダウンロードする簡単なプログラムから始めましょう。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
次のダウンロードが開始される前に 2 秒間のダウンロードが完了する必要があるため、プログラムには合計 6 秒かかります。これを視覚化してみましょう:
この時間を短くすることができます。go ルーチンを使用するようにプログラムを変更しましょう:
注意: 関数呼び出しの前にキーワードを移動
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
待って何?何も印刷されませんでしたか?なぜ?
何が起こっているのかを理解するために、これを視覚化してみましょう。
上記の視覚化から、ゴルーチンが終了する前に main 関数が存在することがわかります。観察の 1 つは、すべての goroutine のライフサイクルが main 関数に依存しているということです。
注: main 関数自体は goroutine です ;)
これを修正するには、他のゴルーチンが完了するまでメインのゴルーチンを待機させる方法が必要です。これを行うにはいくつかの方法があります:
Go ルーチンが完了するまで 数秒待ちます。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
これの問題は、ゴルーチンにどれくらいの時間がかかるかわからないことです。場合によっては、それぞれの時間は一定ですが、実際のシナリオでは、ダウンロード時間は変化することがわかっています。
Go の sync.WaitGroup は、ゴルーチンのコレクションの実行が完了するのを待つために使用される同時実行制御メカニズムです。
ここで、実際の動作を見て視覚化してみましょう:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
これを視覚化して、sync.WaitGroup の動作を理解しましょう:
カウンターメカニズム:
同期フロー:
避けるべき一般的な落とし穴
package main
import (
"fmt"
"time"
)
func downloadFile(filename string) {
fmt.Printf("Starting download: %s\n", filename)
// Simulate file download with sleep
time.Sleep(2 * time.Second)
fmt.Printf("Finished download: %s\n", filename)
}
func main() {
fmt.Println("Starting downloads...")
startTime := time.Now() // Record start time
go downloadFile("file1.txt")
go downloadFile("file2.txt")
go downloadFile("file3.txt")
// Wait for goroutines to finish
time.Sleep(3 * time.Second)
elapsedTime := time.Since(startTime)
fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
これで、ゴルーチンがどのように機能するかをよく理解できました。では、2 つの go ルーチンはどのように通信するのでしょうか?ここでチャンネルの出番です。
Go のChannel は、ゴルーチン間の通信に使用される強力な同時実行プリミティブです。これらは、ゴルーチンがデータを安全に共有する方法を提供します。
チャネルをパイプとして考えてください: 1 つのゴルーチンはデータをチャネルに送信でき、別のゴルーチンはそれを受信できます。
ここにいくつかのプロパティがあります:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
ch <- "hello" がデッドロックを引き起こすのはなぜですか?チャネルは本質的にブロックしており、ここでは「hello」を渡しているため、レシーバーが存在するまでメインのゴルーチンがブロックされますが、レシーバーがないためスタックします。
ゴルーチンを追加してこれを修正しましょう
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
これを視覚化してみましょう:
今回のメッセージは、別の goroutine から送信されているため、チャネルへの送信中にメインはブロックされません。そのため、メッセージは msg := <-ch に移動し、そこでメッセージを受信するまでメインの goroutine をブロックします。メッセージ。
次に、チャネルを使用してファイル ダウンローダーの問題を解決しましょう (メインは他のプログラムが終了するのを待ちません)。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() // Record start time go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") // Wait for goroutines to finish time.Sleep(3 * time.Second) elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
視覚化:
理解を深めるために予行演習をしてみましょう:
プログラム開始:
メインの goroutine が Done チャネルを作成します
3 つのダウンロードゴルーチンを起動します
各ゴルーチンは同じチャネルへの参照を取得します
ダウンロードの実行:
チャンネルループ:
ループ動作:
完了順序は関係ありません!
所見:
⭐ 各送信 (完了 <- true) には、受信 (<-完了) が 1 つだけあります
⭐ メインのゴルーチンはループを通じてすべてを調整します
2 つのゴルーチンがどのように通信できるかはすでに見てきました。いつ?この間ずっと。 main 関数も goroutine であることを忘れないでください。
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
これを視覚化して予行演習してみましょう:
ドライラン:
プログラム開始 (t=0ms)
最初のメッセージ (t=1ms)
2 番目のメッセージ (t=101ms)
3 番目のメッセージ (t=201ms)
チャネルクローズ (t=301ms)
完了 (t=302-303ms)
なぜバッファリングされたチャネルが必要なのでしょうか?
バッファなしチャネルは、相手側の準備が整うまで送信側と受信側の両方をブロックします。高頻度の通信が必要な場合、両方のゴルーチンがデータ交換のために一時停止する必要があるため、バッファーのないチャネルがボトルネックになる可能性があります。
バッファリングされたチャネルのプロパティ:
実際に動作している様子を確認します:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
出力 (ch<-"third" のコメントを解除する前)
なぜメインのゴルーチンをブロックしなかったのでしょうか?
バッファリングされたチャネルでは、送信者をブロックせずにその容量まで送信できます。
チャネルの容量は 2 です。これは、ブロックする前にバッファーに 2 つの値を保持できることを意味します。
バッファは「first」と「next」ですでにいっぱいです。これらの値を同時に消費する受信者が存在しないため、送信操作は無期限にブロックされます。
メインの goroutine も送信を担当しており、チャネルから値を受信するアクティブな goroutine が他にないため、プログラムは 3 番目のメッセージを送信しようとするとデッドロックに入ります。
3 番目のメッセージのコメントを解除すると、現在容量がいっぱいであるためデッドロックが発生し、バッファが解放されるまで 3 番目のメッセージはブロックされます。
Aspect | Buffered Channels | Unbuffered Channels |
---|---|---|
Purpose | For decoupling sender and receiver timing. | For immediate synchronization between sender and receiver. |
When to Use | - When the sender can proceed without waiting for receiver. | - When sender and receiver must synchronize directly. |
- When buffering improves performance or throughput. | - When you want to enforce message-handling immediately. | |
Blocking Behavior | Blocks only when buffer is full. | Sender blocks until receiver is ready, and vice versa. |
Performance | Can improve performance by reducing synchronization. | May introduce latency due to synchronization. |
Example Use Cases | - Logging with rate-limited processing. | - Simple signaling between goroutines. |
- Batch processing where messages are queued temporarily. | - Hand-off of data without delay or buffering. | |
Complexity | Requires careful buffer size tuning to avoid overflows. | Simpler to use; no tuning needed. |
Overhead | Higher memory usage due to the buffer. | Lower memory usage; no buffer involved. |
Concurrency Pattern | Asynchronous communication between sender and receiver. | Synchronous communication; tight coupling. |
Error-Prone Scenarios | Deadlocks if buffer size is mismanaged. | Deadlocks if no goroutine is ready to receive or send. |
: の場合、バッファリングされた
チャネルを使用します: の場合、バッファなし
チャネルを使用しますこれらの基本は、より高度な概念への準備を整えます。今後の投稿では、以下について説明します:
次の投稿:
Go の強力な同時実行機能について理解を深めていきますので、ご期待ください!
以上が直感的なビジュアルで Golang のゴルーチンとチャネルを理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。