序章

Go言語の人気のある機能の1つは、同時実行のファーストクラスのサポート、またはプログラムが一度に複数のことを実行できることです。 コンピューターが単一のコードストリームをより高速に実行することから、より多くのコードストリームを同時に実行することへと移行するにつれて、コードを同時に実行できることはプログラミングの大きな部分になりつつあります。 プログラムをより高速に実行するには、プログラマーはプログラムを同時に実行するように設計する必要があります。これにより、プログラムの各同時部分を他の部分から独立して実行できます。 Goの2つの機能、goroutineschannelsは、一緒に使用すると並行性を容易にします。 Goroutinesは、プログラムで同時コードを設定して実行することの難しさを解決し、チャネルは、同時に実行されるコード間で安全に通信することの難しさを解決します。

このチュートリアルでは、ゴルーチンとチャネルの両方を探索します。 まず、goroutinesを使用して複数の関数を一度に実行するプログラムを作成します。 次に、そのプログラムにチャネルを追加して、実行中のゴルーチン間で通信します。 最後に、プログラムにさらにゴルーチンを追加して、複数のワーカーゴルーチンで実行されているプログラムをシミュレートします。

前提条件

このチュートリアルに従うには、次のものが必要です。

  • バージョン1.16以降をインストールしてください。 これを設定するには、オペレーティングシステムのGoチュートリアルをインストールする方法に従ってください。
  • Goチュートリアルで関数を定義および呼び出す方法にあるGo関数に精通していること。

Goroutinesと同時に関数を実行する

最近のコンピューターでは、プロセッサー、つまり CPU は、できるだけ多くのコードストリームを同時に実行するように設計されています。 これらのプロセッサには1つ以上の「コア」があり、それぞれが同時に1つのコードストリームを実行できます。 したがって、プログラムが同時に使用できるコアが多いほど、プログラムの実行速度は速くなります。 ただし、プログラムがマルチコアが提供する速度の向上を利用するには、プログラムを複数のコードストリームに分割できる必要があります。 プログラムをパーツに分割することは、プログラミングで行うのが難しいことの1つですが、Goはこれを簡単にするように設計されています。

Goがこれを行う1つの方法は、goroutinesと呼ばれる機能を使用することです。 ゴルーチンは、他のゴルーチンも実行されているときに実行できる特殊なタイプの関数です。 プログラムが一度に複数のコードストリームを実行するように設計されている場合、プログラムは同時にを実行するように設計されています。 通常、関数が呼び出されると、実行を継続した後、コードの前に完全に実行が終了します。 これは、プログラムが終了する前に他のことを実行できないため、「フォアグラウンド」での実行と呼ばれます。 ゴルーチンを使用すると、関数呼び出しは、ゴルーチンが「バックグラウンド」で実行されている間、すぐに次のコードを実行し続けます。 コードが終了する前に他のコードの実行を妨げない場合、コードはバックグラウンドで実行されていると見なされます。

パワーゴルーチンが提供するのは、各ゴルーチンをプロセッサコアで同時に実行できることです。 コンピュータに4つのプロセッサコアがあり、プログラムに4つのゴルーチンがある場合、4つのゴルーチンすべてを同時に実行できます。 このように、コードの複数のストリームが異なるコアで同時に実行されている場合、それはparallelでの実行と呼ばれます。

並行性と並列性の違いを視覚化するために、次の図を検討してください。 プロセッサが関数を実行するとき、最初から最後まで一度に実行されるとは限りません。 関数がファイルの読み取りなど、他の何かが発生するのを待っているときに、オペレーティングシステムがCPUコア上の他の関数、ゴルーチン、または他のプログラムをインターリーブする場合があります。 この図は、並行性のために設計されたプログラムが、単一のコアと複数のコアでどのように実行できるかを示しています。 また、単一のコアで実行する場合よりも、並列で実行する場合に、ゴルーチンのより多くのセグメントが同じ時間枠にどのように収まるか(図に示すように9つの垂直セグメント)を示します。

図の左側の列に「同時実行性」というラベルが付いているのは、同時実行性を中心に設計されたプログラムが、 goroutine1、次に別の関数、ゴルーチン、またはプログラム、次に goroutine2、 それから goroutine1 再び、等々。 ユーザーには、これは、実際には小さな部分で次々に実行されているにもかかわらず、プログラムがすべての関数またはゴルーチンを同時に実行しているように見えます。

図の右側の「並列処理」というラベルの付いた列は、2つのCPUコアを搭載したプロセッサで同じプログラムを並列に実行する方法を示しています。 最初のCPUコアは goroutine1 他の関数、ゴルーチン、またはプログラムが散在して実行されている間、2番目のCPUコアは goroutine2 そのコアで他の関数またはgoroutineを実行します。 時々両方 goroutine1goroutine2 異なるCPUコア上で、互いに同時に実行されています。

この図は、Goのもう1つの強力な特性であるスケーラビリティも示しています。 プログラムは、数個のプロセッサコアを備えた小さなコンピューターから、数十個のコアを備えた大きなサーバーまで、あらゆるもので実行でき、それらの追加リソースを利用できる場合、スケーラブルです。 この図は、ゴルーチンを使用することにより、並行プログラムが単一のCPUコアで実行できることを示していますが、CPUコアが追加されると、より多くのゴルーチンを並行して実行してプログラムを高速化できます。

新しい並行プログラムを開始するには、 multifunc 選択した場所のディレクトリ。 プロジェクト用のディレクトリがすでにある場合がありますが、このチュートリアルでは、次のディレクトリを作成します。 projects. あなたは作成することができます projects IDEまたはコマンドラインを介してディレクトリ。

コマンドラインを使用している場合は、最初に projects ディレクトリとそのディレクトリへの移動:

  1. mkdir projects
  2. cd projects

から projects ディレクトリ、を使用します mkdir プログラムのディレクトリを作成するコマンド(multifunc)そしてそれにナビゲートします:

  1. mkdir multifunc
  2. cd multifunc

あなたが入ったら multifunc ディレクトリ、という名前のファイルを開きます main.go を使用して nano、またはお気に入りのエディター:

  1. nano main.go

次のコードを貼り付けるか、次のコードを入力します main.go 開始するファイル。

projects / multifunc / main.go
package main

import (
	"fmt"
)

func generateNumbers(total int) {
	for idx := 1; idx <= total; idx++ {
		fmt.Printf("Generating number %d\n", idx)
	}
}

func printNumbers() {
	for idx := 1; idx <= 3; idx++ {
		fmt.Printf("Printing number %d\n", idx)
	}
}

func main() {
	printNumbers()
	generateNumbers(3)
}

この初期プログラムは、2つの機能を定義します。 generateNumbersprintNumbers、次にこれらの関数をで実行します main 関数。 The generateNumbers 関数は、パラメータとして「生成」する数値の量(この場合は1から3)を受け取り、それらの各数値を画面に出力します。 The printNumbers 関数はまだパラメータを取りませんが、1から3までの数字も出力します。

保存したら main.go ファイル、を使用して実行します go run 出力を確認するには:

  1. go run main.go

出力は次のようになります。

Output
Printing number 1 Printing number 2 Printing number 3 Generating number 1 Generating number 2 Generating number 3

関数が次々に実行されるのがわかります。 printNumbers 最初に実行し、 generateNumbers 2番目に実行されます。

さて、想像してみてください printNumbersgenerateNumbers それぞれの実行には3秒かかります。 同期を実行する場合、または最後の例のように次々に実行する場合、プログラムの実行には6秒かかります。 初め、 printNumbers 3秒間実行され、その後 generateNumbers 3秒間実行されます。 ただし、プログラムでは、これら2つの関数は、実行するために他方からのデータに依存しないため、互いに独立しています。 これを利用して、ゴルーチンを使用して関数を同時に実行することにより、この架空のプログラムを高速化できます。 両方の機能が同時に実行されている場合、理論的には、プログラムは半分の時間で実行できます。 両方の場合 printNumbers そしてその generateNumbers 関数の実行には3秒かかり、両方がまったく同時に開始され、プログラムは3秒で終了する可能性があります。 (ただし、実際の速度は、コンピューターに搭載されているコアの数や、コンピューターで同時に実行されている他のプログラムの数など、外部要因によって異なる場合があります。)

ゴルーチンとして関数を同時に実行することは、関数を同期的に実行することに似ています。 (標準の同期関数ではなく)関数をゴルーチンとして実行するには、関数を追加するだけです。 go 関数呼び出しの前にキーワード。

ただし、プログラムでgoroutineを同時に実行するには、さらに1つの変更を加える必要があります。 両方のゴルーチンの実行が終了するまでプログラムが待機する方法を追加する必要があります。 あなたがあなたのgoroutinesが終わるのを待たずにそしてあなたの main 関数が完了すると、ゴルーチンが実行されない可能性があります。または、一部のみが実行され、実行が完了しません。

関数が終了するのを待つには、GoのsyncパッケージのWaitGroupを使用します。 The sync パッケージには、次のような「同期プリミティブ」が含まれています WaitGroup、プログラムのさまざまな部分を同期するように設計されています。 あなたの場合、同期は両方の関数の実行が終了したことを追跡するので、プログラムを終了できます。

The WaitGroup プリミティブは、使用するのを待つ必要があるものの数を数えることによって機能します Add, Done、 と Wait 機能。 The Add 関数は、関数に提供された数だけカウントを増やし、 Done カウントを1つ減らします。 The Wait 次に、関数を使用して、カウントがゼロに達するまで待機できます。つまり、 Done の呼び出しを相殺するのに十分な回数呼び出されました Add. カウントがゼロに達すると、 Wait 関数が戻り、プログラムは実行を継続します。

次に、のコードを更新します main.go ファイルを使用して、両方の関数をgoroutineとして実行します。 go キーワードを追加し、 sync.WaitGroup プログラムへ:

projects / multifunc / main.go
package main

import (
	"fmt"
	"sync"
)

func generateNumbers(total int, wg *sync.WaitGroup) {
	defer wg.Done()

	for idx := 1; idx <= total; idx++ {
		fmt.Printf("Generating number %d\n", idx)
	}
}

func printNumbers(wg *sync.WaitGroup) {
	defer wg.Done()

	for idx := 1; idx <= 3; idx++ {
		fmt.Printf("Printing number %d\n", idx)
	}
}

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	go printNumbers(&wg)
	go generateNumbers(3, &wg)

	fmt.Println("Waiting for goroutines to finish...")
	wg.Wait()
	fmt.Println("Done!")
}

宣言した後 WaitGroup、待つべきものがいくつあるかを知る必要があります。 を含む wg.Add(2) の中に main goroutinesを開始する前に機能すると wg 2つ待つ Done グループが終了したと見なす前に呼び出します。 ゴルーチンが開始される前にこれが行われないと、問題が発生したり、コードがパニックになったりする可能性があります。 wg それが何かを待っているべきだとは知らない Done 呼び出します。

その後、各関数はを使用します defer 電話する Done 関数の実行が終了した後、カウントを1つ減らします。 The main 関数も更新され、への呼び出しが含まれるようになりました WaitWaitGroup、だから main 関数は、両方の関数が呼び出されるまで待機します Done 実行を継続し、プログラムを終了します。

保存した後 main.go ファイル、を使用して実行します go run 以前と同じように:

  1. go run main.go

出力は次のようになります。

Output
Printing number 1 Waiting for goroutines to finish... Generating number 1 Generating number 2 Generating number 3 Printing number 2 Printing number 3 Done!

出力はここに印刷されているものとは異なる場合があり、プログラムを実行するたびに変わる可能性があります。 両方の関数が同時に実行されている場合、出力は、Goとオペレーティングシステムが各関数の実行に与える時間によって異なります。 各関数を完全に実行するのに十分な時間があり、両方の関数がシーケンス全体を中断することなく出力することがあります。 また、上記の出力のようにテキストが散在している場合もあります。

あなたが試すことができる実験は、 wg.Wait() で呼び出す main 機能し、プログラムを数回実行します go run また。 コンピュータによっては、からの出力が表示される場合があります generateNumbersprintNumbers 関数ですが、それらからの出力がまったく表示されない可能性もあります。 への呼び出しを削除すると Wait、プログラムは、続行する前に両方の関数の実行が終了するのを待機しなくなります。 以来 main 関数はすぐに終了します Wait 関数、あなたのプログラムが終わりに達する可能性が高いです main goroutinesの実行が終了する前に、機能して終了します。 これが発生すると、いくつかの数字が印刷されますが、各関数の3つすべてが表示されるわけではありません。

このセクションでは、を使用するプログラムを作成しました go 2つのゴルーチンを同時に実行し、数列を出力するキーワード。 あなたも使用しました sync.WaitGroup プログラムを終了する前に、これらのゴルーチンが終了するのをプログラムに待機させます。

お気づきかもしれませんが generateNumbersprintNumbers 関数には戻り値がありません。 Goでは、ゴルーチンは標準関数のように値を返すことができません。 あなたはまだ使用することができます go 値を返す関数を呼び出すためのキーワードですが、それらの戻り値は破棄され、アクセスできなくなります。 では、値を返すことができない場合、あるゴルーチンのデータが別のゴルーチンに必要な場合はどうしますか? 解決策は、「チャネル」と呼ばれるGo機能を使用することです。これにより、あるゴルーチンから別のゴルーチンにデータを送信できます。

チャネルを使用してGoroutine間で安全に通信する

並行プログラミングのより難しい部分の1つは、同時に実行されているプログラムのさまざまな部分の間で安全に通信することです。 注意しないと、並行プログラムでのみ発生する可能性のある問題が発生する可能性があります。 たとえば、データレースは、プログラムの2つの部分が同時に実行されており、一方の部分が変数を更新しようとし、もう一方の部分が同時に変数を読み取ろうとしている場合に発生する可能性があります。 これが発生すると、読み取りまたは書き込みが順不同で発生し、プログラムの一方または両方の部分が間違った値を使用する可能性があります。 「データレース」という名前は、データにアクセスするために互いに「レース」するプログラムの両方の部分に由来しています。

Goでのデータ競合などの同時実行の問題が発生する可能性はありますが、この言語は、それらを回避しやすくするように設計されています。 ゴルーチンに加えて、チャネルは並行性をより安全で使いやすくするもう1つの機能です。 チャネルは、データを送信できる2つ以上の異なるゴルーチン間のパイプのように考えることができます。 1つのゴルーチンがパイプの一方の端にデータを入れ、別のゴルーチンが同じデータを取り出します。 データが一方から他方に安全に移動することを確認することの難しい部分は、あなたのために処理されます。

Goでチャネルを作成するのは、組み込みのスライスを作成する方法と似ています。 make() 関数。 チャネルの型宣言は、 chan キーワードの後に、チャネルで送信するタイプのデータが続きます。 たとえば、送信するためのチャネルを作成するには int 値、あなたはタイプを使用します chan int. 送信するためのチャネルが必要な場合 []byte vaules、それは chan []byte、 そのようです:

bytesChan := make(chan []byte)

チャネルが作成されると、矢印のようなものを使用して、チャネルでデータを送受信できます。 <- オペレーター。 の位置 <- チャネル変数に関連する演算子は、チャネルからの読み取りかチャネルへの書き込みかを決定します。

チャネルに書き込むには、チャネル変数から始めて、その後に <- 演算子、次にチャネルに書き込みたい値:

intChan := make(chan int)
intChan <- 10

チャネルから値を読み取るには、値を入れたい変数から始めます。 = また := 変数に値を割り当て、その後に <- 演算子、そしてあなたが読みたいチャンネル:

intChan := make(chan int)
intVar := <- intChan

これらの2つの操作をまっすぐに保つには、次のことを覚えておくと便利です。 <- 矢印は常に左向きです(ではなく ->)、矢印は値がどこに向かっているのかを示します。 チャネルに書き込む場合、矢印は値をチャネルに向けます。 チャネルから読み取る場合、矢印はチャネルを変数に向けます。

スライスのように、チャンネルは range forループのキーワード。 を使用してチャネルを読み取る場合 range キーワードを指定すると、ループの各反復でチャネルから次の値が読み取られ、ループ変数に入れられます。 その後、チャネルが閉じられるか、チャネルが閉じられるまで、チャネルからの読み取りを続行します。 for ループは、次のような他の方法で終了します。 break:

intChan := make(chan int)
for num := range intChan {
	// Use the value of num received from the channel
	if num < 1 {
		break
	}
}

場合によっては、関数がチャネルからの読み取りまたはチャネルへの書き込みのみを許可し、両方を許可したくない場合があります。 これを行うには、を追加します <- 演算子に chan タイプ宣言。 チャネルからの読み取りと書き込みと同様に、チャネルタイプは <- 矢印を使用すると、変数がチャネルを読み取りのみ、書き込みのみ、または読み取りと書き込みの両方に制限できるようになります。 たとえば、の読み取り専用チャネルを定義するには int 値の場合、型宣言は次のようになります <-chan int:

func readChannel(ch <-chan int) {
	// ch is read-only
}

チャネルを書き込み専用にする場合は、次のように宣言します。 chan<- int:

func writeChannel(ch chan<- int) {
	// ch is write-only
}

矢印が読み取り用のチャネルを指し、書き込み用のチャネルを指していることに注意してください。 の場合のように、宣言に矢印がない場合 chan int、チャネルは読み取りと書き込みの両方に使用できます。

最後に、チャネルが使用されなくなったら、組み込みを使用して閉じることができます close() 関数。 チャネルが作成され、プログラム内で何度も使用されないままになると、メモリリークと呼ばれるものが発生する可能性があるため、この手順は不可欠です。 メモリリークとは、プログラムがコンピュータのメモリを使い果たすものを作成したが、使用が完了するとそのメモリをコンピュータに解放しない場合です。 これにより、プログラムは、水漏れのように、時間の経過とともにより多くのメモリを消費するようになります(またはそれほど遅くない場合もあります)。 チャネルが作成されたとき make()、コンピュータのメモリの一部がチャネルに使用された後、 close() チャネルで呼び出されると、そのメモリはコンピュータに戻され、他の目的で使用されます。

今、更新します main.go プログラム内のファイルを使用して chan int あなたのgoroutinesの間で通信するためのチャネル。 The generateNumbers 関数は数値を生成し、チャネルに書き込みます。 printNumbers 関数はチャネルからそれらの番号を読み取り、画面に出力します。 の中に main 関数の場合、他の各関数にパラメーターとして渡す新しいチャネルを作成してから、 close() 使用されなくなるため、チャネルで閉じます。 The generateNumbers また、関数の実行が完了すると、プログラムは必要なすべての数値の生成を終了するため、関数はもはやゴルーチンではありません。 このように、 close() 関数は、両方の関数の実行が終了する前にのみチャネルで呼び出されます。

projects / multifunc / main.go
package main

import (
	"fmt"
	"sync"
)

func generateNumbers(total int, ch chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()

	for idx := 1; idx <= total; idx++ {
		fmt.Printf("sending %d to channel\n", idx)
		ch <- idx
	}
}

func printNumbers(ch <-chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for num := range ch {
		fmt.Printf("read %d from channel\n", num)
	}
}

func main() {
	var wg sync.WaitGroup
	numberChan := make(chan int)

	wg.Add(2)
	go printNumbers(numberChan, &wg)

	generateNumbers(3, numberChan, &wg)

	close(numberChan)

	fmt.Println("Waiting for goroutines to finish...")
	wg.Wait()
	fmt.Println("Done!")
}

のパラメータで generateNumbersprintNumbers、あなたはそれを見るでしょう chan タイプは読み取り専用タイプと書き込み専用タイプを使用しています。 以来 generateNumbers チャネルに番号を書き込むことができる必要があるだけで、書き込み専用タイプであり、 <- チャネルを指す矢印。 printNumbers チャネルから番号を読み取ることができる必要があるだけなので、読み取り専用タイプであり、 <- チャネルから離れる方向を指す矢印。

これらのタイプは chan int、読み取りと書き込みの両方が可能になるため、デッドロックと呼ばれるものからプログラムの実行が誤って停止するのを防ぐために、関数が必要とするものだけに制限することが役立ちます。 デッドロックは、プログラムのある部分がプログラムの別の部分が何かを実行するのを待っているときに発生する可能性がありますが、プログラムの他の部分もプログラムの最初の部分が終了するのを待っています。 プログラムの両方の部分が互いに待機しているため、2つのギアが停止したときのように、プログラムが実行を継続することはありません。

デッドロックは、Goでのチャネル通信の動作方法が原因で発生する可能性があります。 プログラムの一部がチャネルに書き込んでいるとき、プログラムの別の部分がそのチャネルから読み取るまで待機してから続行します。 同様に、プログラムがチャネルから読み取っている場合、プログラムの別の部分がそのチャネルに書き込むまで待機してから続行します。 他の何かが起こるのを待っているプログラムの一部は、何かが起こるまで継続することがブロックされているため、ブロッキングと呼ばれます。 チャネルは、書き込みまたは読み取り時にブロックされます。 したがって、チャネルへの書き込みを期待しているが、代わりに誤ってチャネルから読み取る関数がある場合、チャネルが書き込まれることはないため、プログラムがデッドロックに陥る可能性があります。 これが決して起こらないようにすることは、を使用する理由の1つです。 chan<- int または <-chan int ただの代わりに chan int.

更新されたコードのもう1つの重要な側面は、 close() 書き込みが完了したら、チャネルを閉じます。 generateNumbers. このプログラムでは、 close() を引き起こします for ... range ループイン printNumbers 出る。 使用してから range チャネルからの読み取りは、読み取り元のチャネルが閉じられるまで続きます。 close 呼び出されない numberChan それから printNumbers 決して終わらないでしょう。 もしも printNumbers 決して終わらない、 WaitGroupDone メソッドが呼び出されることはありません defer いつ printNumbers 終了します。 の場合 Done メソッドがから呼び出されることはありません printNumbers、プログラム自体は決して終了しません。 WaitGroupWait のメソッド main 機能は継続しません。 これはデッドロックのもう1つの例です。 main 関数は決して起こらない何かを待っています。

次に、を使用して更新されたコードを実行します go run コマンドオン main.go また。

  1. go run main.go

出力は以下に示すものとわずかに異なる場合がありますが、全体的には同様である必要があります。

Output
sending 1 to channel sending 2 to channel read 1 from channel read 2 from channel sending 3 to channel Waiting for functions to finish... read 3 from channel Done!

プログラムからの出力は、 generateNumbers 関数は、と共有されているチャネルにそれらを書き込んでいる間、1から3までの数字を生成しています printNumbers. 一度 printNumbers 番号を受け取り、それを画面に印刷します。 後 generateNumbers 終了する3つの数値すべてを生成し、 main チャネルを閉じて、 printNumbers 終了しました。 一度 printNumbers 最後の番号の印刷が終了すると、 DoneWaitGroup プログラムが終了します。 以前の出力と同様に、表示される正確な出力は、オペレーティングシステムまたはGoランタイムが特定のゴルーチンの実行を選択した場合など、さまざまな外部要因によって異なりますが、比較的近いはずです。

ゴルーチンとチャネルを使用してプログラムを設計する利点は、分割するようにプログラムを設計した後、それをより多くのゴルーチンにスケールアップできることです。 以来 generateNumbers チャンネルに書き込んでいるだけで、そのチャンネルから他にいくつ読んでいるかは関係ありません。 チャンネルを読み取るものすべてに番号を送信するだけです。 複数を実行することでこれを利用できます printNumbers goroutineなので、それぞれが同じチャネルから読み取り、データを同時に処理します。

プログラムが通信にチャネルを使用しているので、 main.go もう一度ファイルしてプログラムを更新し、複数のプログラムを開始するようにします printNumbers goroutines。 あなたはへの呼び出しを微調整する必要があります wg.Add したがって、開始するゴルーチンごとに1つ追加されます。 に追加することを心配する必要はありません WaitGroup への呼び出しのために generateNumbers ゴルーチンとして実行していたときとは異なり、プログラムは関数全体を終了せずに続行されないためです。 それが減少しないことを確実にするために WaitGroup それが終了したときにカウントします、あなたは削除する必要があります defer wg.Done() 関数からの行。 次に、ゴルーチンの番号をに追加します printNumbers チャンネルがそれぞれによってどのように読み取られるかを簡単に確認できます。 生成される数値の量を増やすことも、数値が分散していることを確認しやすくするために良い考えです。

projects / multifunc / main.go
...

func generateNumbers(total int, ch chan<- int, wg *sync.WaitGroup) {
	for idx := 1; idx <= total; idx++ {
		fmt.Printf("sending %d to channel\n", idx)
		ch <- idx
	}
}

func printNumbers(idx int, ch <-chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for num := range ch {
		fmt.Printf("%d: read %d from channel\n", idx, num)
	}
}

func main() {
	var wg sync.WaitGroup
	numberChan := make(chan int)

	for idx := 1; idx <= 3; idx++ {
		wg.Add(1)
		go printNumbers(idx, numberChan, &wg)
	}

	generateNumbers(5, numberChan, &wg)

	close(numberChan)

	fmt.Println("Waiting for goroutines to finish...")
	wg.Wait()
	fmt.Println("Done!")
}

一度あなたの main.go が更新されたら、を使用してプログラムを再度実行できます go runmain.go. プログラムは3つ開始する必要があります printNumbers 数の生成に進む前に、goroutines。 また、プログラムは3つではなく5つの数値を生成する必要があります。これにより、3つの数値のそれぞれに数値が分散していることがわかりやすくなります。 printNumbers goroutines:

  1. go run main.go

出力は次のようになります(ただし、出力はかなり異なる場合があります)。

Output
sending 1 to channel sending 2 to channel sending 3 to channel 3: read 2 from channel 1: read 1 from channel sending 4 to channel sending 5 to channel 3: read 4 from channel 1: read 5 from channel Waiting for goroutines to finish... 2: read 3 from channel Done!

今回のプログラム出力を見ると、上記の出力とは大きく異なる可能性があります。 3つあるので printNumbers goroutinesが実行されている場合、どちらが特定の番号を受け取るかを決定するチャンスの要素があります。 いつ printNumbers goroutineは番号を受け取り、その番号を画面に印刷するのに少し時間がかかりますが、別のgoroutineはチャネルから次の番号を読み取り、同じことを行います。 ゴルーチンが番号の印刷作業を終了し、別の番号を読み取る準備ができると、ゴルーチンは戻ってチャネルを再度読み取り、次の番号を印刷します。 チャネルから読み取る番号がなくなると、次の番号を読み取ることができるようになるまでブロックを開始します。 一度 generateNumbers 終了し、 close() チャネルで呼び出され、3つすべて printNumbers goroutinesは彼らを終えます range ループして終了します。 3つのゴルーチンがすべて終了して呼び出されたとき DoneWaitGroupWaitGroupのカウントはゼロに達し、プログラムは終了します。 生成されるゴルーチンまたは数値の量を増減して、それが出力にどのように影響するかを確認することもできます。

ゴルーチンを使用するときは、始めすぎないようにしてください。 理論的には、プログラムには数百または数千のゴルーチンが含まれる可能性があります。 ただし、プログラムを実行しているコンピューターによっては、ゴルーチンの数を増やすと実際には遅くなる可能性があります。 ゴルーチンの数が多いと、リソース不足に遭遇する可能性があります。 Goがゴルーチンの一部を実行するたびに、次の関数でコードを実行するために必要な時間に加えて、実行を再開するために少し余分な時間が必要になります。 余分な時間がかかるため、実際にゴルーチン自体を実行するよりも、コンピューターが各ゴルーチンの実行を切り替えるのに時間がかかる可能性があります。 これが発生すると、プログラムとそのゴルーチンが実行に必要なリソースを取得していないか、リソースが非常に少ないため、リソース不足と呼ばれます。 このような場合、同時に実行するプログラムの部分の数を減らすと、それらを切り替えるのにかかる時間が短縮され、プログラム自体の実行により多くの時間がかかるため、より高速になる可能性があります。 プログラムが実行されているコアの数を覚えておくことは、使用するゴルーチンの数を決定するための良い出発点になります。

ゴルーチンとチャネルの組み合わせを使用すると、小さなデスクトップコンピュータでの実行から大規模なサーバーまで拡張できる非常に強力なプログラムを作成できます。 このセクションで見たように、チャネルを使用して、最小限の変更で、わずか数個のゴルーチンから潜在的に数千個のゴルーチンの間で通信できます。 プログラムを作成するときにこれを考慮に入れると、Goで利用可能な同時実行性を利用して、ユーザーに全体的なエクスペリエンスを向上させることができます。

結論

このチュートリアルでは、を使用してプログラムを作成しました go 実行時に数値を出力する同時実行のgoroutineを開始するキーワード。 そのプログラムが実行されたら、次の新しいチャネルを作成しました int 使用する値 make(chan int)次に、チャネルを使用して1つのゴルーチンで数値を生成し、それらを別のゴルーチンに送信して画面に印刷します。 最後に、チャネルとゴルーチンを使用してマルチコアコンピューターでプログラムを高速化する方法の例として、同時に複数の「印刷」ゴルーチンを開始しました。

Goでの並行性について詳しく知りたい場合は、Goチームによって作成された EffectiveGoドキュメントでさらに詳しく説明します。 並行性は並列処理ではありませんGoのブログ投稿は、並行性と並列処理の関係についての興味深いフォローアップでもあります。2つの用語は、同じことを意味すると誤って混同されることがあります。

このチュートリアルは、 DigitalOcean How to Code inGoシリーズの一部でもあります。 このシリーズでは、Goの初めてのインストールから、言語自体の使用方法まで、Goに関する多くのトピックを取り上げています。