2017年1月7日土曜日

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

勉強の為に引用しました。


  • 144
    いいね
  • 3
    コメント
に更新

 この記事は最終更新日から1年以上が経過しています。

この記事の目的

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.

0 コメント:

コメントを投稿