🙌

コンビニのレジで学ぶGoの並行処理入門

2023/09/09に公開

自分は最近Goを勉強し始めた駆け出しエンジニア学生なのですが「並行処理何も分からん」状態だったので、現実世界の並行処理と結びつけてプログラムを書いてみました!トレースの実行方法についても触れています。

並行処理はコンビニのレジがちょうどいい例になるのでは?と思い、勉強のアウトプットとしてコンビニのレジと結び付けて学べる記事を作成しました。最近入門したばかりなので、なにか間違いや、もっといいやり方があれば教えていただけると幸いです!

参考

今回の記事を作成するにあたってこちらの記事を参考にさせていただきました。
こちらに詳しく書かれているので、並行処理の詳細な説明については省略させていただきます。
https://zenn.dev/hsaki/books/golang-concurrency

状況

レジの田中くんはいつも1人でレジをしています。ここのコンビニはお昼はとても繁盛していて、ピーク時にはレジ待ち行列ができています。田中くんは毎日懸命にレジをこなして列がなくなるまで働き続けます。

コードで書く

実際にこの状況をコードで表してみたいと思います。

お客さん

まずはお客さんを並べる関数を作ります。お客さんの列はチャネルで表現し、10人のお客さんを並べます。Goではチャネルを作成するときは外部からの予期せぬ変更を防ぐために、読み取り専用のチャネルを返す関数を作ることが推奨されているので、これに乗っ取って読み取り専用チャネルを返却します。このチャネルをgo routine間で共有することで、お客さん(リソース)を共有したり、お客さんの取り合い(デッドロック)などを防ぐことができます。

main.go
// お客さんを並べる。
func CustomerGenerator() <-chan int {
	customerQueue := make(chan int, 100)
	for i := 0; i < 10; i++ {
		customerQueue <- i
	}
	close(customerQueue)
	return customerQueue
}

レジ係

次にレジでお客さんの対応をする関数を作ります。レジではお客さんの列を引数に受取り、ランダム時間お客さんの対応し、お客さんの列が0になるまで懸命に働き続けます。
ここでは、どのように処理されているのかを確認するためトレースを実行するために、親のコンテキストを受けとり、トレースを実行しています。

main.go
func HitCashRegister(ctx context.Context, wg *sync.WaitGroup, customerQueue *<-chan int, name string) {
	wg.Add(1)
	go func() {
		//cashRegisterのトレース開始
		defer trace.StartRegion(ctx, name).End()
		defer wg.Done()
		for {
			if num, ok := <-*customerQueue; ok {
				fmt.Printf("%s「%d人目のお客さんのレジ打ちをしています、、、」\n", name, num+1)
				time.Sleep(time.Duration(rand.Int31n(5)) * time.Second)
				fmt.Printf("%s「次の方どうぞ!!」\n", name)
			} else {
				break
			}
		}
		fmt.Printf("%s「もう列がない!」\n", name)
	}()
}

main関数

最後にmain関数です。
トレース用のコードを用意して、処理の流れを確認できるようにします。こちらに詳しく書かれています。
https://zenn.dev/hsaki/books/golang-concurrency/viewer/analysis
wait()ではすべてのwaitGroupがなくなるまでそこで処理がストップします。

main.go
func main() {
	//ファイル作成
	file, err := os.Create("trace.out")
	if err != nil {
		log.Fatalln("Error:", err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			log.Fatalln("Error:", err)
		}
	}()

	//トレース開始
	if err := trace.Start(file); err != nil {
		log.Fatalln("Error:", err)
	}
	defer trace.Stop()
	ctx, task := trace.NewTask(context.Background(), "main")
	defer task.End()

	//お客さんを並べる。読み取り専用のチャネルを作成する。
	customerQueue := CustomerGenerator()

	fmt.Println("レジ打ち開始")
	var wg sync.WaitGroup
	HitCashRegister(ctx, &wg, &customerQueue, "cashRegister1") //レジを作成
	wg.Wait()
	fmt.Println("帰るぞー!")
}

実行

実行すると以下の結果が得られます。

実行結果
cashRegister1「1人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「2人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「3人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「4人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「5人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「6人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「7人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「8人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「9人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「10人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「もう列がない!」
帰るぞー!

トレース結果も確認します。以下のコマンドでブラウザが立ち上がり確認できます。

$ go tool trace trace.out

ここをUser-defined tasks→Count 1→(goroutine view)の順に進んでいくと、「いつどんなtask/regionが実行されていたか」というのが視覚的に確認できます。

今回はレジが終わるま約21秒かかっていることが分かります。

並行処理にする

ここで田中くんは何か思いつきます。
田中「レジの人数増やせばお客さんもっと早く終われるんじゃ、、、?ぼく天才かも」

ということで、レジの人数を3人に増やしてみます。ここでは関数を複数呼び出し、go routineを3つ立ち上げています。

main.go
func main() {
	//ファイル作成
	file, err := os.Create("trace.out")
	if err != nil {
		log.Fatalln("Error:", err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			log.Fatalln("Error:", err)
		}
	}()

	//トレース開始
	if err := trace.Start(file); err != nil {
		log.Fatalln("Error:", err)
	}
	defer trace.Stop()
	ctx, task := trace.NewTask(context.Background(), "main")
	defer task.End()

	//お客さんを並べる。読み取り専用のチャネルを作成する。
	customerQueue := CustomerGenerator()

	fmt.Println("レジ打ち開始")
	var wg sync.WaitGroup
	HitCashRegister(ctx, &wg, &customerQueue, "cashRegister1")
+	HitCashRegister(ctx, &wg, &customerQueue, "cashRegister2")
+	HitCashRegister(ctx, &wg, &customerQueue, "cashRegister3")
	wg.Wait()
	fmt.Println("帰るぞー!")
}

同様に実行すると以下の結果が得られます。

実行結果
レジ打ち開始
cashRegister3「1人目のお客さんのレジ打ちをしています、、、」
cashRegister1「2人目のお客さんのレジ打ちをしています、、、」
cashRegister2「3人目のお客さんのレジ打ちをしています、、、」
cashRegister3「次の方どうぞ!!」
cashRegister3「4人目のお客さんのレジ打ちをしています、、、」
cashRegister2「次の方どうぞ!!」
cashRegister2「5人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「6人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「7人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister1「8人目のお客さんのレジ打ちをしています、、、」
cashRegister3「次の方どうぞ!!」
cashRegister3「9人目のお客さんのレジ打ちをしています、、、」
cashRegister2「次の方どうぞ!!」
cashRegister2「10人目のお客さんのレジ打ちをしています、、、」
cashRegister1「次の方どうぞ!!」
cashRegister3「次の方どうぞ!!」
cashRegister3「もう列がない!」
cashRegister1「もう列がない!」
cashRegister2「次の方どうぞ!!」
cashRegister2「もう列がない!」
帰るぞー!

トレース結果を確認すると、並行で処理が実行されていることが確認できます!また並行処理にすることによって実行時間が約10秒となり、およそ2倍の速さで実行することができました!
これで田中くんも満足そうです!

最後に

今回はとても簡単な例を取り扱いましたが、Goの並行処理はまだまだ奥が深いので、お客さんが継続的に流れてきたり、レンジでご飯を温めたりなどの要素も追加してみたいなと思いました。

拙い記事ですがここまで読んでいただきありがとうございます!

Discussion