12
4

More than 5 years have passed since last update.

goroutineと仲良くする(1)

Last updated at Posted at 2017-12-05

はじめに

Go言語が採用される一つの理由である、"並行"実行が簡単に書くことができるということです。
そんなGo言語の"並行"実行を叶えるgoroutine(&& channel)をまとめようと思います。
(自分の勉強も含めて)

実行環境

version
OS Mac HighSierra 10.13.1
Go 1.9.2

goroutine

複数のタスクが同時に実行される関数のことです。
goroutineはスレッドを使用しているので、スレッドに似ているものを想定していただいて構いませんが、
スレッドよりも軽量なのでより多くのgoroutineを生成することができます。

channel

並行で実行されているgoroutine間の通信のためにあります。
ある1つのgoroutineが別のgoroutineに何かを渡したい場合に使用します。

並行と並列の違い

Go言語がgoroutineとして提供しているのは"並行"処理です。(並列処理も可能)
"並行"処理と"並列"処理は混同しがちなので、その2つの違いを明確にしておきます。
Go言語の偉大なる開発者の一人であるRob Pike氏も動画で触れています。動画のリンク
複数のタスクを同時に実行することに違いはありません。
並行処理は1つのリソースを使用して、順不同もしくは同時にタスクが実行されることです。
並列処理は複数を使用して、同時に複数のタスクが実行されることです。

goroutineの基本的な使用方法

使用方法は(無名)関数の前にgoキーワードをつけて記述するだけです。

package main

import (
    "fmt"
    "time"
)

func printNumbers(begin, end int) {
    for i := begin; i < end; i++ {
        fmt.Println(i)
    }
}

func main() {
    fmt.Println("--- start ---")

    printNumbers(1, 5)
    go printNumbers(6, 10)  // goroutine1
    go printNumbers(11, 15) // goroutine2

    time.Sleep(100 * time.Millisecond) // 0.1秒遅延
    fmt.Println("--- finish ---")
}

※ プログラムの処理が非同期処理も早く終了してしまって結果が出力されないので0.1秒遅延させてます。

出力結果1
--- start ---
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- finish ---

printNumbers(1, 5)こちらは普通に関数呼び出しをされているので実行される順番は変わりませんが、
go printNumbers(6, 10)go printNumbers(11, 15)はgoroutineを使用して関数を呼び出しているので、
何度か実行していると以下のような実行結果になることもあります。

出力結果2
--- start ---
1
2
3
4
5
11
12
13
14
15
6
7
8
9
10
--- finish ---

今回の例だとgoroutineを使って呼び出している関数の処理がとても軽量なので、
関数呼び出しごとのまとまった出力が得られますが、以下のように呼び出す関数にも遅延(処理時間を増やす)を追加すれば、また結果が変わります。

package main

import (
    "fmt"
    "time"
)

func printNumbers(begin, end int) {
    for i := begin; i <= end; i++ {
        time.Sleep(1 * time.Millisecond) // 遅延を追加
        fmt.Println(i)
    }
}

func main() {
    fmt.Println("--- start ---")

    printNumbers(1, 5)
    go printNumbers(6, 10)  // goroutine1
    go printNumbers(11, 15) // goroutine2

    time.Sleep(100 * time.Millisecond) // 0.1秒遅延
    fmt.Println("--- finish ---")
}
出力結果2
--- start ---
1
2
3
4
5
6
11
7
12
8
13
9
14
10
15
--- finish ---

1から5までは普通の関数呼び出しなので連続して出力されていますが、
それ以降はgoroutine1goroutine2の処理が順不同に行われているのがわかります。

goroutine同士の通信

1つのgoroutineから別のgoroutineへ何かを渡したい場合は、
channel機能を利用することで可能です。

以下にchannelを使用した基本的な同期処理を記述します。

package main

import (
    "fmt"
    "time"
)

func countUpNumbers(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i // channelへ格納
        fmt.Println("countup:", i)
    }
}

func catchNumber(ch chan int) {
    for i := 0; i < 5; i++ {
        num := <-ch // channelから取り出し
        fmt.Println("catch:  ", num)
    }
}

func main() {
    fmt.Println("--- start ---")

    ch := make(chan int)
    go countUpNumbers(ch)
    go catchNumber(ch)

    time.Sleep(100 * time.Millisecond) // 0.1秒遅延
    fmt.Println("--- end ---")
}
出力結果3
--- start ---
catch:   0
countup: 0
countup: 1
catch:   1
catch:   2
countup: 2
countup: 3
catch:   3
catch:   4
countup: 4
--- end ---

catchcountupから出力される数字が同期されている数字であることがわかります。
送信した数字を受信側で受け取るまで送信側は処理を中断するので、このように順番にカウントが増えていきます。

バッファ付きchannelの実装も可能です。
いままで使用していたのはバッファなしチャンネルです。
データをキューに格納し、FIFO形式で扱われます。
実装方法は、ch := make(chan int, [バッファサイズ])のように指定するだけです。

ベンチ計測

goroutineを使用した並行実行速度を確認してみようと思います。
goはベンチ計測も標準で用意されていて素晴らしいですね。

bench_test.go
package bench

import (
    "testing"
    "time"
)

const COUNT = 100

func printNumbers(begin, end int) {
    var numbers []int
    for i := begin; i < end; i++ {
        numbers = append(numbers, i)
    }
}

func BenchmarkPrintNumbers(b *testing.B) {
    printNumbers(1, COUNT)         // goroutine1
    printNumbers(COUNT+1, COUNT*2) // goroutine2
}

func BenchmarkGoPrintNumbers(b *testing.B) {
    b.ResetTimer()
    go printNumbers(1, COUNT)         // goroutine1
    go printNumbers(COUNT+1, COUNT*2) // goroutine2

    time.Sleep(100 * time.Millisecond) // 0.1ms sleep
}
$ go test -bench . -cpu 1
BenchmarkPrintNumbers       2000000000           0.00 ns/op
BenchmarkGoPrintNumbers     2000000000           0.05 ns/op
PASS

結果を見て見るとgoroutineを使用している方が時間がかかっています。
並行実行で行うタスク自体が軽量すぎると、並行実行にすることのコストの方が上回ってしまいます。

ただこのままではgoroutineの性能が発揮されていないので、少しだけ処理を重く(遅延)を入れてみます。

package bench

import (
    "testing"
    "time"
)

const COUNT = 100

func printNumbers(begin, end int) {
    var numbers []int
    for i := begin; i < end; i++ {
        time.Sleep(1 * time.Millisecond)
        numbers = append(numbers, i)
    }
}

func BenchmarkPrintNumbers(b *testing.B) {
    printNumbers(1, COUNT)         // goroutine1
    printNumbers(COUNT+1, COUNT*2) // goroutine2
}

func BenchmarkGoPrintNumbers(b *testing.B) {
    b.ResetTimer()
    go printNumbers(1, COUNT)         // goroutine1
    go printNumbers(COUNT+1, COUNT*2) // goroutine2

    time.Sleep(100 * time.Millisecond) // 0.1ms sleep
}
$ go test -bench . -cpu 1
BenchmarkPrintNumbers       1000000000           0.27 ns/op        0 B/op          0 allocs/op
BenchmarkGoPrintNumbers     2000000000           0.05 ns/op        0 B/op          0 allocs/op
PASS

0.1秒遅延をいれて見てみると、
普通に関数を呼び出したパターンとgoroutineを使用しているパターンを見比べてみると
5倍ほどの実行時間の差が出ているのがわかります。

まとめ

今回はgoroutineの基本的な使用方法についてまとめてみました。
さらにgoroutineを活用や検証するのは次回、次々回の記事でやろうと思います。

参考

12
4
2

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
12
4