🔍

Goroutineを使用した並列処理と直列処理の計測

2024/10/01に公開

はじめに

Goの並列処理を学習するにあたって、実際に同期処理と非同期処理でどれくらい実行完了時間が変わるのか計測してみようと思います。実際にGoでの並列処理についてもアウトプットします。
今回のプログラムについては、いろんな都市の天気を取得できる外部APIを5回リクエストします。その内容と合計でかかった実行時間を出力するといった内容となっています。
Goのバージョンは1.23を使用しています。

並列処理の実装

  • Goroutine
    Goroutineは、Go言語における並行処理を実現するための軽量なスレッドのようなものです。Goroutineは非常に軽く、1つのプログラムで何千ものGoroutineを作成して並行に動作させることが可能です。
    go キーワードを使って、関数の呼び出しを並行処理として実行します。
  • sync.WaitGroup
    sync.WaitGroup は、Goroutineの完了を待つための同期プリミティブです。Goroutineが複数ある場合、sync.WaitGroupを使うことで全てのGoroutineの終了を待つことができます。主に次の3つのメソッドを使います。
    • Add(int): 実行するGoroutineの数を指定します。
    • Done(): 各Goroutineの処理が終わったことを通知します。deferで最後に実行されるようにします。
    • Wait(): 全てのGoroutineの終了を待ちます。
main.go(並列処理)
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"
)

// 天気APIのレスポンスから気温を抽出するための構造体
type WeatherResponse struct {
	Main struct {
		Temp float64 `json:"temp"`
	} `json:"main"`
}

func fetchWeather(city string, wg *sync.WaitGroup) {
	defer wg.Done()

	url := fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s", city, API_KEY)
	resp, err := http.Get(url)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	var weather WeatherResponse
	if err := json.Unmarshal(body, &weather); err != nil {
		fmt.Println("Error parsing JSON:", err)
		return
	}

	fmt.Println(city, weather.Main.Temp-273.15) // ケルビンから摂氏に変換(温度の表示形式変換)
}

func main() {
	cities := []string{"Tokyo", "Texas", "London", "Paris", "Sydney"}
	start := time.Now()
	var wg sync.WaitGroup

	// 並列にリクエスト
	for _, city := range cities {
		wg.Add(1)
		go fetchWeather(city, &wg)
	}

	wg.Wait()
	duration := time.Since(start)
	fmt.Printf("Total Time: %v\n", duration)
}

直列処理(同期処理)の実装

main.go(直列処理)
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

// 天気APIのレスポンスから気温を抽出するための構造体
type WeatherResponse struct {
	Main struct {
		Temp float64 `json:"temp"`
	} `json:"main"`
}

func fetchWeather(city string) {

	url := fmt.Sprintf("http://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s", city, API_KEY)
	resp, err := http.Get(url)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	var weather WeatherResponse
	if err := json.Unmarshal(body, &weather); err != nil {
		fmt.Println("Error parsing JSON:", err)
		return
	}

	fmt.Println(city, weather.Main.Temp-273.15) // ケルビンから摂氏に変換(温度の表示形式変換)
}

func main() {
	cities := []string{"Tokyo", "Texas", "London", "Paris", "Sydney"}
	start := time.Now()
	// 並列にリクエスト
	for _, city := range cities {
		fetchWeather(city)
	}

	duration := time.Since(start)
	fmt.Printf("Total Time: %v\n", duration)
}

計測結果

実行ログは下記の通りです。並列処理が約0.3秒、直列処理が約0.7秒となっており、並列処理の方が約2倍処理時間が早いことがわかりました。

並列処理
% go run main.go
Tokyo 24.760000000000048
Paris 15.110000000000014
Sydney 16.82000000000005
Texas 15.970000000000027
London 14.450000000000045
Total Time: 304.442375ms
直列処理
% go run main.go
Tokyo 24.24000000000001
Texas 14.970000000000027
London 14.879999999999995
Paris 15.420000000000016
Sydney 16.32000000000005
Total Time: 760.322917ms

感想

今回は、かなり基礎的な内容になったが、今後はプログラムの処理の実行単位までを深く探求していきたい。

Discussion