search
LoginSignup
223

More than 5 years have passed since last update.

posted at

updated at

Organization

Goルーチンで並行化する方法: 6秒かかる処理を3秒にしよう

この記事の目的

6秒かかる直列処理を並行処理に改修し、3秒で終わるようにする

準備

まずは、適当にフォルダを作る

mkdir ~/Desktop/goroutine
cd ~/Desktop/goroutine/

つぎに、main.goファイルを作る

touch main.go

まず直列実行するプログラムを書く

  • 1秒待つコマンド
  • 2秒待つコマンド
  • 3秒待つコマンド

上記を順番に実行していくプログラムを main.go の中身を書く。
つまり、合計で6秒かかってしまう。

main.go
package main

import (
    "log"
    "time"
)

func main() {
    log.Print("started.")

    // 1秒かかるコマンド
    log.Print("sleep1 started.")
    time.Sleep(1 * time.Second)
    log.Print("sleep1 finished.")

    // 2秒かかるコマンド
    log.Print("sleep2 started.")
    time.Sleep(2 * time.Second)
    log.Print("sleep2 finished.")

    // 3秒かかるコマンド
    log.Print("sleep3 started.")
    time.Sleep(3 * time.Second)
    log.Print("sleep3 finished.")

    log.Print("all finished.")
}


ともあれ、実行してみる

% go run main.go
2013/12/05 15:02:09 started.
2013/12/05 15:02:09 sleep1 started.
2013/12/05 15:02:10 sleep1 finished.
2013/12/05 15:02:10 sleep2 started.
2013/12/05 15:02:12 sleep2 finished.
2013/12/05 15:02:12 sleep3 started.
2013/12/05 15:02:15 sleep3 finished.
2013/12/05 15:02:15 all finished.

やはり、6秒かかった。

これを並行化したい。

Goルーチンを使って並行化し、3秒で終わるようにする

Go言語にはGoルーチンという処理の並行化を簡単にできる仕組みがある。クロージャーや関数に go をつけるだけなのでシンプル。

ただし、Goルーチンだけでは、並行化した処理が終わる前に、メインの処理が終わってしまう。つまり、待ってくれない。Goルーチンが終わるまで待つには、「チャネル」という仕組みを使う。

なお、各ルーチンの実行結果を、呼び出し元に戻すときにもチャネルを使う。今回は、特に実行結果は必要ないので、チャネルに適当な値を入れることにする。ここでは、とりあえずbool値にする。

以上を踏まえて、さきほどの main.go を改修する:

main.go
package main

import (
    "log"
    "time"
)

func main() {
    log.Print("started.")

    // チャネル
    sleep1_finished := make(chan bool)
    sleep2_finished := make(chan bool)
    sleep3_finished := make(chan bool)

    go func() {
        // 1秒かかるコマンド
        log.Print("sleep1 started.")
        time.Sleep(1 * time.Second)
        log.Print("sleep1 finished.")
        sleep1_finished <- true
    }()

    go func() {
        // 2秒かかるコマンド
        log.Print("sleep2 started.")
        time.Sleep(2 * time.Second)
        log.Print("sleep2 finished.")
        sleep2_finished <- true
    }()

    go func() {
        // 3秒かかるコマンド
        log.Print("sleep3 started.")
        time.Sleep(3 * time.Second)
        log.Print("sleep3 finished.")
        sleep3_finished <- true
    }()

    // 終わるまで待つ
    <- sleep1_finished
    <- sleep2_finished
    <- sleep3_finished

    log.Print("all finished.")
}

実行してみよう

% go run main.go
2013/12/05 15:14:58 started.
2013/12/05 15:14:58 sleep1 started.
2013/12/05 15:14:58 sleep2 started.
2013/12/05 15:14:58 sleep3 started.
2013/12/05 15:14:59 sleep1 finished.
2013/12/05 15:15:00 sleep2 finished.
2013/12/05 15:15:01 sleep3 finished.
2013/12/05 15:15:01 all finished.

並行化ができて、3秒で終わるようになった\(^o^)/

チャネルが冗長なので1つにしたい…

sleep1_finishedsleep2_finishedsleep3_finishedの3つのチャネルを作ったが、もっとエレガントにチャネルしたい。

main.go
package main

import (
    "log"
    "time"
)

func main() {
    log.Print("started.")

    // チャネル
    finished := make(chan bool)

    go func() {
        // 1秒かかるコマンド
        log.Print("sleep1 started.")
        time.Sleep(1 * time.Second)
        log.Print("sleep1 finished.")
        finished <- true
    }()

    go func() {
        // 2秒かかるコマンド
        log.Print("sleep2 started.")
        time.Sleep(2 * time.Second)
        log.Print("sleep2 finished.")
        finished <- true
    }()

    go func() {
        // 3秒かかるコマンド
        log.Print("sleep3 started.")
        time.Sleep(3 * time.Second)
        log.Print("sleep3 finished.")
        finished <- true
    }()

    // 終わるまで待つ
    <-finished
    <-finished
    <-finished

    log.Print("all finished.")
}

チャネルはひとつにできたが、待つところで3回待つ必要がある…。
10並行したら10回 <-finished を書かないといけないのは面倒だし、エンバグしそうなので、回数を指定したい。

「終わるまで待つ」ところを回数指定にする

とりあえず無骨に for で3回 <-finished を実行すれば良いようだ

main.go
package main

import (
    "log"
    "time"
)

func main() {
    log.Print("started.")

    // チャネル
    finished := make(chan bool)

    go func() {
        // 1秒かかるコマンド
        log.Print("sleep1 started.")
        time.Sleep(1 * time.Second)
        log.Print("sleep1 finished.")
        finished <- true
    }()

    go func() {
        // 2秒かかるコマンド
        log.Print("sleep2 started.")
        time.Sleep(2 * time.Second)
        log.Print("sleep2 finished.")
        finished <- true
    }()

    go func() {
        // 3秒かかるコマンド
        log.Print("sleep3 started.")
        time.Sleep(3 * time.Second)
        log.Print("sleep3 finished.")
        finished <- true
    }()

    // 終わるまで待つ
    for i := 1; i <= 3; i++ {
        <-finished
    }

    log.Print("all finished.")
}

実行してみる:

% go run main.go
2013/12/05 15:32:40 started.
2013/12/05 15:32:40 sleep2 started.
2013/12/05 15:32:40 sleep1 started.
2013/12/05 15:32:40 sleep3 started.
2013/12/05 15:32:41 sleep1 finished.
2013/12/05 15:32:42 sleep2 finished.
2013/12/05 15:32:43 sleep3 finished.
2013/12/05 15:32:43 all finished.

回数指定じゃなくて、ルーチンの数だけ待ちたい

<-finsihed を列挙するよりも、回数指定のほうが並行数の増減に対応しやすいが、ルーチンの数に応じて待つようにしたい。

どうやってやるかだが、クロージャーを配列にして、要素の数だけGoルーチンを開始し、要素の数だけ <-finsihed を実行するようにする。この変更を加えたコードが下記になる。

main.go
package main

import (
    "log"
    "time"
)

func main() {
    log.Print("started.")

    finished := make(chan bool)

    // 配列
    funcs := []func(){
        func() {
            // 1秒かかるコマンド
            log.Print("sleep1 started.")
            time.Sleep(1 * time.Second)
            log.Print("sleep1 finished.")
            finished <- true
        },
        func() {
            // 2秒かかるコマンド
            log.Print("sleep2 started.")
            time.Sleep(2 * time.Second)
            log.Print("sleep2 finished.")
            finished <- true
        },
        func() {
            // 3秒かかるコマンド
            log.Print("sleep3 started.")
            time.Sleep(3 * time.Second)
            log.Print("sleep3 finished.")
            finished <- true
        },
    }

    // 並行化する
    for _, sleep := range funcs {
        go sleep()
    }

    // 終わるまで待つ
    for i := 0; i < len(funcs); i++ {
        <-finished
    }

    log.Print("all finished.")
}

実行してみる:

% go run main.go
2013/12/05 16:30:18 started.
2013/12/05 16:30:18 sleep1 started.
2013/12/05 16:30:18 sleep2 started.
2013/12/05 16:30:18 sleep3 started.
2013/12/05 16:30:19 sleep1 finished.
2013/12/05 16:30:20 sleep2 finished.
2013/12/05 16:30:21 sleep3 finished.
2013/12/05 16:30:21 all finished.

まとめ

6秒かかる処理を、Goルーチンとチャネルを組み合わせて並行化し、3秒で終わるようになった。

課題

最後の for あたりをもっとシンプルにする方法はないものか?

UPDATE 2013/12/06 チャネルを使わずに待つ方法

調べてみたら sync.WaitGroup というモジュールがあることがわかった。これを使うとチャネルを宣言しなくても、処理を待つことができる。

main.go
package main

import (
    "log"
    "sync"
    "time"
)

func main() {
    log.Print("started.")

    // 配列
    funcs := []func(){
        func() {
            // 1秒かかるコマンド
            log.Print("sleep1 started.")
            time.Sleep(1 * time.Second)
            log.Print("sleep1 finished.")
        },
        func() {
            // 2秒かかるコマンド
            log.Print("sleep2 started.")
            time.Sleep(2 * time.Second)
            log.Print("sleep2 finished.")
        },
        func() {
            // 3秒かかるコマンド
            log.Print("sleep3 started.")
            time.Sleep(3 * time.Second)
            log.Print("sleep3 finished.")
        },
    }

    var waitGroup sync.WaitGroup

    // 関数の数だけ並行化する
    for _, sleep := range funcs {
        waitGroup.Add(1) // 待つ数をインクリメント

        // Goルーチンに入る
        go func(function func()) {
            defer waitGroup.Done() // 待つ数をデクリメント
            function()
        }(sleep)

    }

    waitGroup.Wait() // 待つ数がゼロになるまで処理をブロックする

    log.Print("all finished.")
}

実行結果:

% go fmt main.go && go run main.go
2013/12/06 16:14:40 started.
2013/12/06 16:14:40 sleep1 started.
2013/12/06 16:14:40 sleep2 started.
2013/12/06 16:14:40 sleep3 started.
2013/12/06 16:14:41 sleep1 finished.
2013/12/06 16:14:42 sleep2 finished.
2013/12/06 16:14:43 sleep3 finished.
2013/12/06 16:14:43 all finished.

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
What you can do with signing up
223