【goroutine】ねこでもわかるGo言語【Features編】

| 0件のコメント


ねこでもわかる Go言語 の第2回目です。

タイトルにもあるように私はねこ好きなんですが, 住まいの都合でペットは飼えないので, 最近はねこ画像を集めて癒されています。
日課になりつつあるのでこれは自動化したい!ということで GoogleのAPIでつくりはじめました。双方向データバインドがしたいためだけにAngular.jsつかってMVCの練習も兼ねたいです。

Go言語の特徴

Goには、特徴的な機能が多くあります。一方でジェネリックがないとか、クラスの継承がないとか、型宣言が逆とか主流言語との相違点もあります。今回はたまたま興味を持った機能を紹介します。

  • 構造体とインターフェイスについて
  • GoにおけるKeyValueCoding
  • goroutineを使ってみる

Go言語見習いがコードを交えながら書きます。環境構築についてはこちらの記事を見てください。

構造体とインターフェイス

Goにはクラスがありませんが, 構造体を使って似たような機能を持たす事ができます。
クラスの継承はありませんが, とって代わる機能として後述するインターフェイスがあります。これにより型で区別するメソッドを作ることができます。

例えば構造体は以下のように書きます。


var rect struct{
	length int 
	name string
}

rect.length = 10
rect.name = "四角"

println("長さ:",rect.length,"名前:",rect.name)

Goでは構造体に直接メソッドを書く事ができません。まずは上記のように構造体中にレシーバ(変数名 型)を定義します。
この構造体のレシーバを使うことでメソッドを定義できます。型にメソッドを持たせるイメージです。


func main(){
	Array := make([]Rect,5)

	for i := 0; i < len(Array); i++{
		Array[i].length = i+1
		Array[i].name = "四角"
		println(Array[i].intro())
	}
}

type Rect struct{
		length int 
		name string
}

func (figure Rect) intro()(str string){
	str = fmt.Sprintf("図形は%sで横の長さは%dMです",figure.name,figure.length)

	return str
}

構造体Rectのメンバ length,nameを経由するメソッドが intro()です。実行結果は以下です。


図形は四角で横の長さは1Mです
図形は四角で横の長さは2Mです
図形は四角で横の長さは3Mです
図形は四角で横の長さは4Mです
図形は四角で横の長さは5Mです

続いて, インターフェイスです。Goのオブジェクト指向はクラスでなくこのインターフェイスによるものです。
インターフェイスとはオブジェクト指向言語ユーザはすでにご存知だと思いますが簡単に言うと”メソッドの定義と実装を切り分け定義のみを定める”ものです。抽象クラスみたいな機能ですね。

さらに, 構造体とinterfaceは継承関係(定義と実装の関係)でなくても構わないらしいです。

インターフェイスを使ったプログラム例です。(冗長なコードだ...)


func main(){
	var GetParam Interface
	var num numbers 
	var str strings

	num.num_1 = 2012
	str.str_1 = "I Like cat."

	GetParam = num
	println(GetParam.GetParam())

	GetParam = str
	println(GetParam.GetParam())
}

type Interface interface{// メソッドの定義
		GetParam() string
}

type numbers struct{
	num_1 int
}

type strings struct{
	str_1 string
}


func (str strings) GetParam()(ret string){// メソッドの実装(文字列)
	ret = fmt.Sprintf("文字列は %s です",str.str_1)

	return ret
}

func (num numbers) GetParam()(ret string){// メソッドの実装(数字)
	ret = fmt.Sprintf("数字は %d です:",num.num_1)

	return ret
}

上記例では構造体を2つ宣言して、2つに対応したメソッドGetParamを実装しています。main内で構造体のメンバを呼び出しています。 


数字は 2012 です
文字列は I Like cat. です

GetParam = num とするか GetParam = str にするかで結果が変化するのがわかると思います。

以上をまとめると,, レシーバを使って構造体にメソッドを持たせることができ, interfaceを使ってから構造体が持つメソッドのリストを作ることができる。

Map

Mapはkeyとvalueで管理できるデータ構造です。この辺は モダンな印象を受けます。使い方ですが, まずmapを宣言します。


var m map[string]int

続いて、makeによってインスタンス化します。(配列なのでnewでなくmakeです)

データを追加してみましょう。


m["a"] = 1

データを消すにはこんな感じで falseを指定します。


m["a"] = 1,false

goroutineを使ってみる

Go言語の目玉機能です。並行処理へのアプローチは CSP(Communicating Sequential Processes) というモデルを参考にされており, UNIXのパイプラインと同様です。詳しくはこちらを参考に。

同一アドレス空間内で他の goroutine同士を並行実行することです。軽量化の工夫として開始時のスタックサイズ (一時メモリ保持)は節約のため小さく取り、必要に応じてヒープ領域を割り当てています。

Goにおける並行処理は以下の2つの特徴があります。

メモリ共有

Goではチャネルを利用することでメモリ共有による同時書き込みの危険性を減らしています。
このチャネルはキュー構造をしており、makeを使って割り当てることができます。
また、ポインタ参照を禁止(非推奨)することでデータ共有をさけています。この方法をメッセージパッシングと呼びます。

マルチコア対応

自動でコンテキストを切り替えを行うことで、マルチコアで実行が可能です。
ここでいうコンテキストとは、制御フローの切り替えであり手動の場合、これをプログラムに記述する必要があります。

実際に goroutineを使った簡単な例です。非同期処理したい関数の前に go を追加するだけです。pthred等に比べ手軽ですね。


func main(){
	value_1 := ch_1();
	value_2 := ch_2();

	for i:= 0; i <= 10;i++ {
        println("ST1:",<- value_1,"\t","ST2:",<- value_2);
    };
}


func ch_1 () chan int {
	ch := make(chan int );

	go func(){
		for i := 0;i <= 10;i++{
		ch <- i;
		}
	}();

	return ch;
}

func ch_2 () chan int {
	ch := make(chan int );

	go func(){
		for i := 10;i <= 10;i--{
		ch <- i;
		}
	}();

	return ch;
}

上記のようにチャネルを経由することでデータのやり取りができます。

ch <- i によって、goroutineの無名関数内でchannelにデータを送信しています。ch_1はchannelで受け取ったデータを返します。 それを main 関数内で変数 stream_1 に入れて表示した結果が以下です。


ST1: 0 	 ST2: 10
ST1: 1 	 ST2: 9
ST1: 2 	 ST2: 8
ST1: 3 	 ST2: 7
ST1: 4 	 ST2: 6
ST1: 5 	 ST2: 5
ST1: 6 	 ST2: 4
ST1: 7 	 ST2: 3
ST1: 8 	 ST2: 2
ST1: 9 	 ST2: 1

関数の引数に関数を指定すること(クロージャ)もできます。

使う CPU Core数を変化させて, goroutineを使ったプログラムの実行時間を計測してみます。使うマシンは 4 Cores搭載の 1.3 GHz Intel Core i5 です。まずは gorouthineを使わない場合です。forループ 100億回 を計算させます。


package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	cpus := runtime.NumCPU()
	fmt.Println("Your Machine Has", cpus, "cores")

	start := time.Now().UnixNano()

	x := 0
	for i := 0; i < 10000000000; i++ { // 100億
		x += 1
	}
	fmt.Println("sum :", x)

	end := time.Now().UnixNano()

	fmt.Println(float64(end-start)/float64(1000000), "ms")
}

4423 ms かかりました。


$ go run no-goroutine.go
Your machine has 4 cores
sum : 10000000000
4423.711072 ms

続いて, goroutineで forループ 100億回 を 10 goroutineで分散させます。


package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker() <-chan int {
	receiver := make(chan int)
	s := 10
	for i := 0; i < s; i++ {
		go func(i int) {
			x := 0
			for j := 0; j < 1000000000; j++ {
				x += 1
			}
			fmt.Println(i, ":", x)
			receiver <- x
		}(i)
	}
	return receiver
}

func main() {
	cpus := runtime.NumCPU()
	fmt.Println("Your Machine Has", cpus, "cores")

	start := time.Now().UnixNano()

	receiver := worker()
	y := 0
	for i := 0; i < 10; i++ {
		y += <-receiver
	}

	fmt.Println("sum :", y)
	end := time.Now().UnixNano()

	fmt.Println(float64(end-start)/float64(1000000), "ms")
}

4コア搭載のマシンなので, GOMAXPROCS環境変数 を 1~4の間で動かして プロセスを起動させます。


$ GOMAXPROCS=1 go run goroutine.go
4057.920576 ms
$ GOMAXPROCS=2 go run goroutine.go
2209.118826 ms
$ GOMAXPROCS=3 go run goroutine.go
2372.733596 ms
$ GOMAXPROCS=4 go run goroutine.go
2204.119871 ms

1 coreの場合は goroutine を使わない場合とほぼ同じ実行時間でした。2-4 coresの場合は 1 coreと比較すると 約6割 実行時間が短くなりました。
3 coresが若干遅いように見えますが, 何度か実行して 平均 を取れば他とも変わらないと思います。

おわりに

他にも紹介しきれない特徴が Go にはあります。私も始めて2週間なので間違った情報を書いてしまっているかもしれませんが、意欲に免じて許して頂ければと思います...


[1] A Multi-threaded Go Raytracer
[2] effective_go

コメントを残す

必須欄は * がついています