LoginSignup
85
69

More than 5 years have passed since last update.

go言語初心者が図を書きながらgoroutineやgo channelを理解する(Part1)

Last updated at Posted at 2016-03-28

自分の理解を深めるために、図を書いて少しずつ(baby stepsで)goroutineやgo channelを理解していきたいと思います。

並行と並列の違い

まずは混同しやすい並行(concurrent)と並列(parallel)の違いを意識しておきたいと思います。(英語の方がわかりやすい気がするのは気のせいでしょうか)
検索すると色々と定義に違いがあって、私ごときがいい加減なことを書くと、すぐにマサカリで頭を真っ二つにされてしまうことがわかりました。
Erlangを作ったJoe Armstrongの5歳児にも分かる例えの図がわかりやすいので引用します。
Concurrent and Parallel Programming

ざっくりいうと、並行処理では共通のリソース(例えばCPUやメモリ)をやりくりしながら複数のタスクをさばいています。
並列処理は、それぞれ独立したリソースを使って複数のタスクをさばいています。

ちなみにRob Pike先生の有名なスライド"Concurrency is not Parallelism"も一読することをおすすめします。

goroutineで並行処理

goroutineを使って簡単な並行処理を書いてみます。
まずは1ミリ秒のSleepを1000回実行するタスクAと1ミリ秒のSleepを2000回実行するタスクBを作成してみます。
ちょっと遅いことが体感できる繰り返し回数にしています。
まずはタスクAとタスクBを順次処理するとどうなるか見てみます。

TaskAtoB.png

try_goroutine.go
package main

import (
    "fmt"
    "time"
)

type Task struct {
    name             string
    process_millisec int 
}

func (t *Task) ProcessTask() {
    fmt.Printf("Start %s\n", t.name)
    for i := 0; i < t.process_millisec; i++ {
        time.Sleep(1 * time.Millisecond)
    }   
    fmt.Printf("Finished %s\n", t.name)
}

// 共通で使用するタスクの定義
var TaskA = Task{name: "A", process_millisec: 1000}
var TaskB = Task{name: "B", process_millisec: 2000}

// 順次実行
func NormalShori() {
    TaskA.ProcessTask()
    TaskB.ProcessTask()
}

func main() {
}

実行時間を見たり、CPUの数を増やしたり、したいのでtestingパッケージを使って実行してみます。
テスト用のコードは以下の通りです。

try_goroutine_test.go
package main

import (
    "testing"
)

func TestNormalShori(t *testing.T) {
    NormalShori()
}

実行時間が見えるようにtestに-vオプション(詳細出力)をつけて実行します。

$ go test -v
=== RUN   TestNormalShori
Start A
Finished A
Start B
Finished B
--- PASS: TestNormalShori (3.84s)
PASS
ok      <snip />    3.849s

タスクAとタスクBを順次実行すると3秒ほどかかることがわかります。

次にgoroutineを使って並行処理を実装します。
try_goroutine.goに以下を追加します。
頭にgoをつけて関数を呼び出すだけです。

TaskAandB.png

try_goroutine.go
// goroutine で実行
func GoShori() {
    go TaskA.ProcessTask()
    go TaskB.ProcessTask()
}

テストコードには以下を追加します。

try_goroutine_test.go
unc TestGoShori(t *testing.T) {
    GoShori()
}

Go1.5以降は実行するマシンに搭載されているCPUを可能な限り使うようになっているので、-cpuオプションを使って1つのCPUで実行するようにします。

$ go test -v -cpu 1
=== RUN   TestNormalShori
Start A
Finished A
Start B
Finished B
--- PASS: TestNormalShori (3.97s)
=== RUN   TestGoShori
--- PASS: TestGoShori (0.00s)
PASS
ok      <snip />    3.983s

goroutineを使ったテストケースでは何も出力されませんでした。
goroutineが標準出力に文字列を出力する前に、テストが終わったからです。
通常はやらない実装ですが、バッファとしてテストケースにSleep処理を入れると少し見えるようになります。

テストケースにSleepを入れた実行例
$ go test -v -cpu 1
=== RUN   TestNormalShori
Start A
Finished A
Start B
Finished B
--- PASS: TestNormalShori (3.99s)
=== RUN   TestGoShori
Start B
Start A
--- PASS: TestGoShori (0.00s)
PASS
ok      <snip />    3.997s

上の実行例ではgoroutineを使ってタスクB、タスクAの順に実行されたことがわかります。
もちろん順次処理よりも早いことがわかります。

0.00sとなっている理由は、ミリ秒ではなく、ナノ秒の単位で見ないとわからないくらい実行時間が短かったからです。

どのくらい早いか知るにはBenchmarkを使わないといけませんが、ここでは順次処理よりも早いということがわかれば良しとします。

WaitGroupを使ってコントロールする

goroutineで実行される全ての処理を完了してから、ある処理を実行するケースを考えてみます。
例えばタスクAとタスクBを平行に実行して、両方のタスクが終わってからタスクCを実行したいとします。
そんな時はsyncパッケージのWaitGroupを使います。

TaskAandBtoC.png

WaitGroupは全てののgoroutineが終わるのを待ってくれます。
WaitGroupにはカウンタという概念があって、WaitGroupに登録する処理の数だけ登録します。
カウンタにgoroutineの数を登録するにはAdd関数を使います。
各処理が終了したらDone関数を使ってカウンタをデクリメントします。
Wait関数はカウンタが0になるまで待ってくれます。

try_waitgroup.go
package main

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

type Task struct {
    name             string
    process_millisec int 
}

func (t *Task) ProcessTask(wg *sync.WaitGroup) {
    fmt.Printf("Start %s\n", t.name)
    for i := 0; i < t.process_millisec; i++ {
        time.Sleep(1 * time.Millisecond)
    }   
    fmt.Printf("Finished %s\n", t.name)
    wg.Done() //カウンタをデクリメント
}

var TaskA = Task{name: "A", process_millisec: 1000}
var TaskB = Task{name: "B", process_millisec: 2000}
var TaskC = Task{name: "C", process_millisec: 3000}
var Tasks []Task = []Task{TaskA, TaskB}

func GoShori() {
    var wg1 sync.WaitGroup
    var wg2 sync.WaitGroup

    for _, task := range Tasks {
        wg1.Add(1) //wg1のカウンタをインクリメント
        go func(task Task) {
            go task.ProcessTask(&wg1)
        }(task)
    }   
    wg1.Wait() //wg1の処理が全て完了するまで待つ

    wg2.Add(1)
    go TaskC.ProcessTask(&wg2)
    wg2.Wait()
}
try_waitgroup_test.go
package main

import "testing"

func TestGoShori(t *testing.T) {
    GoShori()
}
WaitGroup実行結果
$ go test -v -cpu 1
=== RUN   TestGoShori
Start B
Start A
Finished A
Finished B
Start C
Finished C
--- PASS: TestGoShori (6.62s)
PASS
ok      <snip />    6.631s

先ほどの例でFinished Cなどは、標準出力に出ていませんでしたが、Wait関数のおかげで結果が確認できます。

気になるのは実行時間です。
タスクAは1秒、タスクBは2秒、タスクCは3秒ほどかかるので、合計時間を見ると、最初のWaitGroup(wg1)内で並行処理しているように見えません。
おそらくWaitGroupの処理に時間がかかっていると思います。

試しに、タスクA、タスクB、タスクCをTasks配列に入れてWaitGroupを使って実行してます。(2つ目のWaitGroupは削除して、タスクCを1つ目のWaitGroupに追加します)

TaskAandBandC.png

タスクA、タスクB、タスクCを1つのWaitGroupで実行
$ go test -v -cpu 1
=== RUN   TestGoShori
Start C
Start A
Start B
Finished A
Finished B
Finished C
--- PASS: TestGoShori (3.82s)
PASS
ok      <snip />    3.828s

3.8秒と順次処理よりは早い結果が得られました。

長くなったので、Channelの話は次にまわします。

85
69
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
85
69