ゴルーチンは、効率的な並行性を達成するためにGoの初期バージョンから設計された軽量スレッドです。歴史的に、軽量スレッドのアイデアは、システムスレッドのコストを回避し、スケーラブルなサーバーアプリケーションの高い需要に応えるために生まれました。Goはもともと、数百万のタスクが並行して処理される必要があるサーバーおよびネットワークシステムのための言語として設計されました。
問題:並行実行は、ゴルーチンのライフサイクルを管理せず、スケジューリングを考慮せず、終了を管理しなければ、レースコンディションやデッドロック、メモリ消費の増加を引き起こす可能性があります。
解決策:ゴルーチンは、goキーワードを使用して起動されます。ゴルーチンの作業はGoのスケジューラによって計画され、M:Nモデル(MはOSスレッド、NはGo言語のゴルーチン)を使用します。ライフサイクルの管理には、チャネル、WaitGroup、コンテキスト、およびチャネルのクローズ管理を用います。
コードの例:
package main import ("fmt"; "time") func worker(id int) { fmt.Printf("ワーカー %d が開始されました ", id) time.Sleep(time.Second) fmt.Printf("ワーカー %d が終了しました ", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }
主な特徴:
mainでゴルーチンを明示的に待機しない場合、常に実行されますか?
いいえ、mainの実行が終了すると、プロセスは子ゴルーチンの状態に関係なく終了し、すべてのタスクが実行されるわけではありません。
ループからのgo func(...)の起動は、各ゴルーチンがループ変数の独自の値を取得することを保証しますか?
いいえ、ループ変数のキャプチャに問題が発生し、ゴルーチンは同じスライス/変数の値で動作する場合があります。変数をコピーして、引数として渡す必要があります:
for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }
1つのゴルーチンがGoのスケジューラをブロックし、他のゴルーチンの実行を妨げることはありますか?
はい、無限ループや非常に重いループがスイッチポイントなしで起動される場合(例えば、時間の呼び出しやyieldなし)、OSスレッドを占有する可能性があります。これはGoの「協調的マルチタスク」の理念に反します。例えば、ブロックのない重い関数:
func busy() { for { // 何の待機やブロッキング呼び出しもなし } }
マイクロサービスで定期的にデータベースから読み込むゴルーチンを起動しますが、リクエストのキャンセル時に終了するのを忘れます。その結果、時間と共にメモリを消費する「ぶら下がりゴルーチン」が残ります。
利点:
欠点:
タスクのキャンセルを管理するためにコンテキストを使用し、アプリケーションを停止する前にすべてのゴルーチンを管理するためにWaitGroupを使用し、ゴルーチン間で適切にデータを渡すためにチャネルを使用します。
利点:
欠点: