0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Google Colabで学ぶGo並列実行

0
Last updated at Posted at 2026-07-03

Go言語には、goroutineという並行処理の仕組みがあります。

関数呼び出しの前に go をつけることで、その処理を別の流れで実行できます。

たとえば、時間のかかる処理を3つ実行するとします。

普通に書くと、

処理Aが始まる
処理Aが終わる
処理Bが始まる
処理Bが終わる
処理Cが始まる
処理Cが終わる

という順番になります。

しかし、処理A・処理B・処理Cが互いに独立しているなら、本当は順番に待つ必要はありません。

処理A
処理B
処理C

を同時に始められれば、全体の待ち時間を短くできます。

本記事では、Google Colab上でGoのコードを動かしながら、Goの並列実行・並行処理の雰囲気を学びます。

なお、厳密にはgoroutineは「並行処理」の仕組みです。実際にCPU上で本当に同時に実行されるかは環境や処理内容によります。ただし、APIアクセス、ファイル読み込み、待ち時間の多い処理などでは、複数の処理を同時に進めることで、実行速度を高速化できる場合があります。

対象読者

この記事は、次のような人を対象にしています。

  • Google Colabを使ったことがある人
  • Go言語の基本文法を学んだことがあり、簡単なGoのコードなら読める人
  • Goのgoroutineを最初に試してみたい人
  • 並列実行・並行処理の雰囲気をつかみたい人

本格的な並行処理の設計というより、まずは「Goではこういうことができるのか」という感覚をつかむことを目的にしています。

Google Colabは標準ではPythonの実行環境ですが、Linuxコマンドを実行できるため、Goもインストールできます。

学習を進めるために

なお、「Google ColabでGoのプログラムを動かすなんてできるの?」で引っかかってる読者は、まず下記記事で予習してください。

また、先に、Go言語の基本文法が知りたいという人は、下記記事を見てください。

すぐに使えるチートシートはこちら Google Colab版

すぐに実行して、試せるコードレシピはこちら。

時間がかかる処理の例・「2秒かかる処理」を作る

まず、2秒かかる処理を作ります。

実際の業務であれば、

  • APIを呼び出す
  • ファイルを読み込む
  • 画像を変換する
  • CSVを処理する
  • 外部サービスからレスポンスを待つ

といった処理が「時間のかかる処理」になります。

今回は入門用・たんなる「遅い処理」の見本なので、time.Sleep を使って「2秒かかる処理」を再現します。

%%writefile sequencial_execution.go
package main

import (
    "fmt"
    "time"
)

// 2秒間かかるタスク
func task(name string) {
    fmt.Println(name, "start")

    time.Sleep(2 * time.Second)

    fmt.Println(name, "finish")
}

func main() {
    start := time.Now()

    task("A")
    task("B")
    task("C")

    fmt.Println("全部終了")
    fmt.Println("かかった時間:", time.Since(start))
}

実行します。

A start
A finish
B start
B finish
C start
C finish
全部終了
かかった時間: 6.0020938s

task は1回あたり2秒かかります。

それを3回、順番に実行しているため、

2秒 + 2秒 + 2秒 = 約6秒

かかります。

コードの流れとしても、

task("A")
task("B")
task("C")

と書いているため、Aが終わるまでBは始まりません。
Bが終わるまでCも始まりません。

前から順番にひとつずつ。これが普通の逐次実行です。

goをつけるとどうなるか──まずは、失敗例から

Goでは、関数呼び出しの前に go をつけると、その関数をgoroutineとして実行できます。早速、先ほどのコードに go をつけてみます。

%%writefile goroutine_failure.go
package main

import (
    "fmt"
    "time"
)

func task(name string) {
    fmt.Println(name, "開始")

    time.Sleep(2 * time.Second)

    fmt.Println(name, "終了")
}

func main() {
    start := time.Now()

    // ただ「go」と付けるだけで、並列処理ができると思ってしまった人
    go task("処理A")
    go task("処理B")
    go task("処理C")

    fmt.Println("全部終了")
    fmt.Println("かかった時間:", time.Since(start))
}

実行結果の例です。

全部終了
処理A 開始
かかった時間: 35.887µs

かなり不思議な結果になりました。1秒未満で終了です。

しかも、go task("処理A") と書いたのに、処理A・処理B・処理Cが最後まで実行されていません。

これは、go をつけた処理が動き始める一方で、main 関数が、その終了を待たずに進行してしまったためです。

つまり、このコードは失敗例であり、次のようなフローになっています。

1.
 処理Aを別の流れで開始しようとする
 処理Bを別の流れで開始しようとする
 処理Cを別の流れで開始しようとする
 
2.
 main関数は待たずに次へ進む

3.
 全部終了 と表示する
 かかった時間を表示する
 
4.
 main関数が終了する
 プログラム全体が終了する

main 関数が終わると、プログラム全体が終了します。

そのため、goroutineで動き始めた処理が、最後まで表示される前にプログラムが終わってしまいます。

goをつけると、別の流れで処理を始められる。
しかし、goをつけただけでは、その処理の終了を待ってくれるわけではない。

とりあえず3秒待ってみる

先ほどの失敗を踏まえて、雑に3秒待ってみます。

各タスクは2秒で終わるはずなので、3秒待てば、処理A・処理B・処理Cは終わるだろう、という考え方です。

%%writefile goroutine_v1.go
package main

import (
    "fmt"
    "time"
)

func task(name string) {
    fmt.Println(name, "開始")

    time.Sleep(2 * time.Second)

    fmt.Println(name, "終了")
}

func main() {
    start := time.Now()

    go task("処理A")
    go task("処理B")
    go task("処理C")

    // 失敗から学んで、「3秒待つ」という教訓を反映
    time.Sleep(3 * time.Second)

    fmt.Println("全部終了")
    fmt.Println("かかった時間:", time.Since(start))
}

実行結果の例です。

処理C 開始
処理A 開始
処理B 開始
処理B 終了
処理A 終了
処理C 終了
全部終了
かかった時間: 3.000169742s

今度は、処理A・処理B・処理Cが最後まで実行され、全体の時間も短くなりました。goroutineで同時に始めると、約3秒で終わっています。

並列・並行処理では開始順と終了順は、必ずしも一致しない

しかし、今回の処理自体は各2秒なので、本来は2秒少しで終わってほしいところです。ここでは、最後に time.Sleep(3 * time.Second) と書いているため、全体として約3秒かかっています。

このコードは、入門用にあえて紹介していますが、あまり望ましい書き方ではありません。まず、実際の処理が何秒で終わるかは、事前にはわからないのが普通です。そして、万一3秒以上かかった場合には、結局バグが生じます。

そのため、本質的に作るべきは、「3秒待つ」ではなく、「3つの処理が全部終わるまで待つ」仕掛けです。

sync.WaitGroupで正しく待つ

複数のgoroutineが全部終わるまで待つには、sync.WaitGroup を使います。

%%writefile goroutine_v2.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func task(name string) {
    fmt.Println(name, "開始")

    time.Sleep(2 * time.Second)

    fmt.Println(name, "終了")
}

func main() {
    start := time.Now()

    var wg sync.WaitGroup

    wg.Add(3)

    go func() {
        task("処理A")
        wg.Done()
    }()

    go func() {
        task("処理B")
        wg.Done()
    }()

    go func() {
        task("処理C")
        wg.Done()
    }()

    wg.Wait()

    fmt.Println("全部終了")
    fmt.Println("かかった時間:", time.Since(start))
}

実行結果の例です。

処理C 開始
処理A 開始
処理B 開始
処理B 終了
処理A 終了
処理C 終了
全部終了
かかった時間: 2.000392763s

今度は、全体の時間が約2秒になりました。

それぞれの処理は2秒かかりますが、3つの処理をほぼ同時に開始しているため、

2秒 + 2秒 + 2秒 = 約6秒

ではなく、2秒の処理を同時に実行し、終了させることができました。

WaitGroupの考え方

WaitGroup は、複数の処理が終わるまで待つためのカウンターのようなものです。

今回のコードでは、3つの処理を待ちます。

wg.Add(3) // これから3個の処理を待つ
wg.Done() // 処理が1個終わりました
wg.Wait() // Addした数だけDoneが呼ばれるのを待つ

フローで示すと、次のとおり

1.wg.Add(3)
 1-1.処理Aが終わる → wg.Done()
 1-2.処理Bが終わる → wg.Done()
 1-3.処理Cが終わる → wg.Done()

2.三つのDoneが呼ばれた
3.wg.Wait()を抜ける
4.全部終了 と表示する

go func() { ... }() という見た目

先ほどのコードには、次のような書き方が出てきました。

go func() {
    task("処理A")
    wg.Done()
}()

これは、名前のない関数(無名関数)をその場で作って、すぐにgoroutineとして実行する書き方です。しかし、まだ見た目に違和感があるかもしれません。

待ち合わせ込みの関数にする

無名関数の活用に違和感があるなら、待ち合わせ込みの関数を作って、さらに読みやすくもできます。
ついでに、defer も使って、もっと洗練させていきます、
defer は、その関数が終わるときに実行する処理を予約する書き方です。

%%writefile goroutine_v4.go
package main

import (
    "fmt"
    "sync"
    "time"
)

func task(name string) {
    fmt.Println(name, "開始")

    time.Sleep(2 * time.Second)

    fmt.Println(name, "終了")
}

func runTask(name string, wg *sync.WaitGroup) {
    defer wg.Done()

    task(name)
}

func main() {
    start := time.Now()

    var wg sync.WaitGroup

    wg.Add(3)

    go runTask("処理A", &wg)
    go runTask("処理B", &wg)
    go runTask("処理C", &wg)

    wg.Wait()

    fmt.Println("全部終了")
    fmt.Println("かかった時間:", time.Since(start))
}

実行結果の例です。

処理C 開始
処理A 開始
処理B 開始

処理B 終了
処理A 終了
処理C 終了
全部終了
かかった時間: 2.000405881s

このコードでは、次の関数を追加しています。

func runTask(name string, wg *sync.WaitGroup) {
    defer wg.Done()

    task(name)
}

runTask は、

taskを実行する
終わったらwg.Done()を呼ぶ

という関数です。

そのため、main 関数側は少し読みやすくなります。

go runTask("処理A", &wg) // 処理Aを別の流れで実行する
go runTask("処理B", &wg) // 処理Bを別の流れで実行する
go runTask("処理C", &wg) // 処理Cを別の流れで実行する

なお、ここで出てくる &wg は、WaitGroupのポインタを関数に渡すための書き方です。ここでは細かいポインタの説明には入りませんが、WaitGroupを別の関数に渡すときは &wgと書く必要があります。(ポインタについては、別記事で開設予定。)

逐次実行とgoroutine実行の違い

goroutineは高速化に便利であるものの、開始順や終了順は保証されなくなります。そのため、goroutineは、

  • 複数URLへのアクセス
  • 複数ファイルの読み込み
  • 複数画像の変換
  • 複数ユーザーへの通知
  • 複数データの個別処理

のように、順番に依存しない、独立した処理に適しています。

一方で、

  • Aの結果を使ってBを実行する
  • Bの結果を使ってCを実行する

というような処理であれば、単純に並行化できません。

まとめ

本記事では、Google Colab上でGoの並列実行・並行処理を試しました。

  • 最初は逐次実行で書く。
  • 次に go をつけて失敗する。
  • 一度 time.Sleep で雑に待つ。
  • その後、WaitGroup を使って正しく待つ。
  • 最後に defer や関数化で少しずつ整える。

今回の学習で重要なのは、次の3点です。

・goをつけると、関数を別の流れで実行できる
・ただし、main関数はgoroutineの終了を自動では待たない
・複数のgoroutineを待つには、WaitGroupを使う

また何より、Google ColabやJupyter Notebookで学ぶと、こうした試行錯誤の過程をそのまま残すことができます。

完成したコードだけを見ると、なぜその形になったのかわかりにくいことがあります。ノートブック上に失敗や修正の過程が残っていると、「なぜこのコードになったのか」が記録できるようになります。

さいごに一言(余談)

今日、ビッグデータというと、モダンなITの仕事、みたいなイメージを持つ人は多いと思います。
しかしその実、ビッグデータは件数が多いので、処理が遅い・検証が難しい(事故が怖い) ことも、課題になりがちだと思います。

データエンジニアリング領域では依然Pythonのシェアが強いものの、Goの最適化・高速化の仕組みも、ビッグデータ領域と相性がよいと思います。

弊社について

本記事を書いている 合同会社インクルーシブソリューションズ は、データ基盤構築・分析基盤設計・システム改善支援を中心に活動している小規模IT法人です。

主な領域は、

  • データマート設計・データパイプライン構築
  • SQL / Python を用いたデータ処理設計
  • BI導入支援・分析基盤の整備
  • 既存システムの運用改善・可視化支援

といった、「データを使える状態にする」ための活動です。

弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?