Goでのチャネルの空読み

Goでのチャネルは使い方を誤ればpanicになりますが、これはGoの潔いところと思えばいいですが、チャネルの使い方で読み出しだけれども変数には代入しないというのがあります。

具体的には値を使わないのだから、単にダミーのリードですが実際の使い方としては終了待ちとして使うことになるんだろうと思います。

非常にシンプルな空読みの確認用のコードですが、チャネルでバッファーを2個用意して、最初の値は空読みすることで、2個目の値が変数xに代入されていることがわかります。

package main

import  "fmt"

func main() {
	ch := make(chan int, 2)
	defer 	close(ch)
	go func() {
		ch <- 100
		ch <- 200
	}()
	<- ch		// dummy read
	x := <- ch
	fmt.Println(x)
}

<実行結果>
200

 

admin

Goのsliceの型はなんでも良い

Goでは何でも型に定義できるかと思いますが(例外があるかどうかは定かではない)、例えば関数もそのまま型として使用できます。

例えば以下のように、

ここでは”func(float64) float64“を型名として使用して、二つの関数をスライスに格納して、それぞれの関数を無名関数の引数(あえて無名関数でなくても良いですが)としてfor rangeループで実行させています。

package main

import (
	"fmt"
	"math"
)


func main() {
	f1 := func(a float64) float64{
		return a*a
	}
	f2 := func(a float64) float64{
		return math.Pow(a, 3)
	}
	
	fs := []func(float64) float64{f1, f2}		// "[]func(float64) float64" is a type

	for _, fss := range fs{
		func(f func(float64) float64){
			fmt.Println(f(3))
		}(fss)
	}
}

他の言語からすると奇妙な記述方法ではありますが、Goではこのような記法も成立するのだと。

 

admin

 

Goのloopclosure問題、

問題というよりも、タイミングがそうだからそうなるよねという話です。

Goのgoroutineでの話で、以下のソースをVScodeで読み込ませるとopenclosureというwarningが出ます。実行結果も期待される値ではなくて、例えば全部40とかになるのですが、なぜか?

それは無名関数に明示的に引数としてvを与えてる訳ではないので、無名関数の中でvの使われる時点(v*2)のv値が使われるからです。具体的には並行処理を起動しても、すぐに各ルーチンが実行される訳でもなくて実行タイミングには遅延があるだろうから、実はvを使おうとしたタイミングではすでにfor range処理が先行したり、あるいは終了している可能性もあるから。

対応方法はv無名関数に引数として渡すのがスマートなやり方になるでしょう。

https://github.com/mushahiroyuki/lgo/blob/main/example/ch10/ex1005.go

package main

import "fmt"

func main() {
	a := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20} //liststart
	ch := make(chan int, len(a))
	for _, v := range a {
		go func() {
			ch <- v * 2
			fmt.Println(":", v)
		}()
	}
	for i := 0; i < len(a); i++ {
		fmt.Print(<-ch, " ")
	} //listend
	fmt.Println()
}

こうすれば、無名関数が不規則なタイミングでその時点のv値を使用することなく、意図した値を引き渡すことができます。別の対応方法としてはvをシャドーイング(v := vをfor rangeの直後に挿入)しても良さそうですが。いずれにしろ並行処理の実行順序は保証されないので、結果の並びは昇順にはなりません。

		go func(v int) {
			ch <- v * 2
			fmt.Println(":", v)
		}(v)

 

admin

 

gobotを使ってTelloを制御する

gobotはrobotics/IoT用のフレームワークですが、サポートモデルとしてTelloが対象に含まれています。

https://gobot.io/blog/2018/04/20/hello-tello-hacking-drones-with-go/

でコマンド制御と動画再生を行っているので、コードを実行してみました。

動画の再生にはmplayer(これは追加インストール必要でした)を使って、goから引き渡しています。

$ brew install mplayer

でインストールします。go getでもmplayerがインストールできますが、外部アプリなのでこちらではない。

<環境構築>

Goはインストール済みの前提で、gobotの他にdji/telloのインストールも必要。これはmod内でしかインストールできません。

% go get -d -u gobot.io/x/gobot/...
% go get gobot.io/x/gobot
% go get gobot.io/x/gobot/platforms/dji/tello

<Telloから画像受け取り再生するコード>

Telloの素のコマンドは意識する必要はなくて関数でwrapされています。明示的に並行処理は使っていなくて、drone.On()はイベント待ちのように見えます。

package main

import (
	"fmt"
	"os/exec"
	"time"

	"gobot.io/x/gobot"
	"gobot.io/x/gobot/platforms/dji/tello"
)

func main() {
	drone := tello.NewDriver("8890")

	work := func() {
		mplayer := exec.Command("mplayer", "-fps", "25", "-")
		mplayerIn, _ := mplayer.StdinPipe()
		if err := mplayer.Start(); err != nil {
			fmt.Println(err)
			return
		}

		drone.On(tello.ConnectedEvent, func(data interface{}) {
			fmt.Println("Connected")
			drone.StartVideo()
			drone.SetVideoEncoderRate(4)
			gobot.Every(100*time.Millisecond, func() {
				drone.StartVideo()
			})
		})

		drone.On(tello.VideoFrameEvent, func(data interface{}) {
			pkt := data.([]byte)
			if _, err := mplayerIn.Write(pkt); err != nil {
				fmt.Println(err)
			}
		})
	}

	robot := gobot.NewRobot("tello",
		[]gobot.Connection{},
		[]gobot.Device{drone},
		work,
	)

	robot.Start()
}

 

動画は外部のアプリを使っているせいか、遅延が大きいのと画素が飛んでいます。gobotの本質とはそれほど関係ないですが、

gobotはこれからも成長していくだろうから、Robotics/IoTアプリ開発はかなり楽になりそうです。

 

admin

 

interfaceの基本(Go言語)

interfaceはGo言語における唯一の抽象型だそうで、抽象型というのは実装の自由度がある訳だから使いこなせば便利でしょう。

ここでは使いこなしではなくて、基本機能の説明です。構造体のデータの操作を行う二つのメソッドを定義しています。

package main

import (
	"fmt"
)

type person struct {
	FirstName	string
	FamilyName	string
	Age			int8
}

type inf interface{
	print()
	exchange()
}

func (p *person) print(){
	fmt.Println(p.FirstName, p.FamilyName, p.Age)
}

func (p *person) exchange(){
	fn := p.FirstName
	p.FirstName = p.FamilyName
	p.FamilyName = fn
	p.print()
}

func main(){
	var i1 inf = &person{
		FirstName: "Mary",
		FamilyName: "Bloody",
		Age: 36,
	}

	i1.print()

	i1.exchange()
}

実行結果は、

Mary Bloody 36
Bloody Mary 36

Goの場合にはJavaやC#のように明示的なimplementは不要です、つまり緩い関係になってます。interfaceで定義したメソッドが全て実装されていればimplementと等価です。

じゃ『interfaceは何が良いの?』というとそれは抽象型だからというところに行き着くでしょう、何故なら実装をどう記述しようがinterfaceに規定したメソッドと同じ形(名称、引数と戻り値)であれば入れ替えできるわけで、実は明示的では無いですが継承や多態性が実現できることになります。それゆえGoにおける唯一の抽象型というのは使い方に多様性があることになります。

例えば、上の例で異なる型を扱う同名のメソッドを定義すれば、それは正しく多態性になる訳だから。

このように型によって緩やかな結合を取ることがGoの一つの大きな特徴であるように思います

P.S. (2023/1/25)

interfaceの型名は~~er(~~するもの)とするのが通例のようですから、命名規則からは外れています。

 

admin

 

値を変更するならポインター渡し(Go言語)

https://isehara-3lv.sakura.ne.jp/blog/2023/01/17/goは値渡し言語、/

で、Goは値渡しなので、渡し先で値を変更するならポインター渡しと言っていますがその例です。

以下のソースで関数printPersonにポインターを渡した時はjohnの値が変更されていますが、値渡しにすると元の値は変更されません。

package main

import (
	"fmt"
)

type Person struct{
	LastName string
	FirstName string
	Age int
}

func (p Person) printPerson(p1 *Person) Person{
	p1.Age = 72
	fmt.Println("John : ", p1.FirstName, p1.LastName, p1.Age)
	return *p1
}

func main() {
	var john Person
	john.FirstName = "John"
	john.LastName = "Wein"
	john .Age = 70
	john.printPerson(&john)
	fmt.Println("returned age : ", john.Age)
}

以下はポインタ渡しと値渡しでの実行結果です、確かに値渡しではオリジナルの情報は更新されません。

<ポインター渡し>
John :  John Wein 72
returned age :  72

<値渡し>
John : John Wein 72
returned age : 70

 

admin

 

 

Goは値渡し言語、

タイトルの通りですがGo言語は値渡し、例えば関数に引き渡す引数は値が引数のアドレスとは違う場所の関数内で使用する変数のアドレスにコピーされて使われます。

これでは都合悪い時もあるだろうから、参照渡しのためにポインターが存在知ると考えれば良いかもしれない。ポインターはC/C++の記法そのままで、

package main

import "fmt"

func main() {
	val := 20
	pointer := &val
	*pointer = 23
	fmt.Println("address : ", pointer, "value : ", val)
}


address :  0x14000126008 value :  23

のように使います、しかしGoでポインターを普段使いしたら、変数がmutableになることでコードの見通しが悪くなるから使うのは限定的だろうと思う。Goでは通常の変数はすべてimmutable扱いだから、逆に言えばポインター宣言は明示的なmutable宣言であるということです。

例にあるようなケースでわざわざポインターを使う必要性はないし、こんな使い方はしちゃいけない例です。数少ない例外はインターフェースを受け取るだけだとは『初めてのGo言語』の記載。

 

admin

 

 

 

Goの並行処理と並列処理、

ダラダラと継続中ですが、とりあえずの現状のまとめです。

<条件>

M1 MacBook air

1000万までの素数計算してスライスに格納、昇順は考慮してない

 

<言えること>

・goはシングルスレッドでもマルチコアを使うように(並列処理するように)動く、これは便利だ

・channelのオーバーヘッドはmutexよりも大きい予想通りですが、ただしmutexのように個別の変数に対する考慮は不要だからエラーの入りにくいのがchannel

・コア数を強制的にしてしてやるとこのコードの場合には4コアぐらいが最適、もちろん状況により変わりますが

それぞれのソースコードはこちら、

https://github.com/chateight/go/tree/master/concpara

 

 

admin

Goの並行処理でchannelを使う

https://isehara-3lv.sakura.ne.jp/blog/2023/01/12/goの並行処理/

ではWaitGroup使って終了を判定しましたが、Go独自の機能にスレッド間での通信機能channelを使う方法もあります。どちらが良いというのではなくて、目的によって使い分けするんだと思いますが。

以下はリンクの処理と同じく一千万までの素数を求める処理です。40行目付近から下のループ処理でchannelを使って素数をスライスに格納しています。channelが存在しないことはタイマーの一秒待ちで判定しています。

package main

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

//
// to use channel instead of "WaitGroup"
//
// in this case, slower than WaitGroup. Channel may be useful when it takes long processing time and less inforamtion size
//
func main() {
	maxNumber := 1000*10000
	var mu sync.Mutex
	ch := make(chan int)
	defer close(ch)
	c := 2
	oddCh := []int{}
	tStart := time.Now()
	tStop := time.Now()
	for i := 2; i <= maxNumber; i++ {
		go func() {
			flag := true		// if odd number, stay "true"
			mu.Lock()
			defer mu.Unlock()
			for j :=2; j*j <= c ; j++ {
				if c%j == 0{
					flag = false
					break
				}
			}
			if flag == true{
				ch <- c
			}			
			c++
		}()
	}
	for i:= 0; i < maxNumber; i++{				// set odd numbers to the slice
		setBreak := false
		select {
		case p := <- ch:
			oddCh = append(oddCh, p)
			tStop = time.Now()
		case <- time.After(time.Second):		// to check last "ch" data
			fmt.Println("Time Out!")
			setBreak = true
		}
		if setBreak == true{
			break
		}
	}
	
	fmt.Println("len : ", len(oddCh))

	el := tStop.Sub(tStart)
	fmt.Println(el)
}

実はWaitGroupを使った場合に比較して、処理時間は5倍ぐらいになっています。おそらく渡しているのが素数というint形式データを渡しているだけでオーバーヘッドが大きい。したがってchannelを使うのに細かなデータをやり取りするには向かないんだろうと思います。

いずれにしろこのような簡単な処理では、goroutineも使わずにgoの実行環境でマルチコアを使う方が一番高速なのだから、余計なことをやらないのがいちばんのようです。

 

admin

 

 

Goの並行処理

Goの並行処理は、関数の前にGoを入れるだけで処理対象になります。リソースの排他処理が必要なときには他の言語と同様なmutexを使う、あるいはチャネルを使っても良さそうです。

<素数を求めるコード>

package main

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

func main() {
	var wg sync.WaitGroup
	var mu sync.Mutex
	c := 2
	odd := []int{}
	tStart := time.Now()
	for i := 2; i <= 10000*1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			flag := true		// if odd number, stay "true"
			mu.Lock()
			defer mu.Unlock()
			for j :=2; j*j <= c ; j++ {
				if c%j == 0{
					flag = false
					break
				}
			}
			if flag == true{
				odd = append(odd, c)
			}			
			c++
		}()
	}
	wg.Wait()
	tStop := time.Now()
	fmt.Println(len(odd))

	el := tStop.Sub(tStart)
	fmt.Println(el)
}

mutexを宣言する場所はこのケースではc++の直前でも良さそうなのですが、この位置じゃないとちゃんと結果が出ません。(TBD)

ちなみにC++との実行速度の比較、

千万までの素数計算をM1 Macでさせると、

C++ : およそ1.5秒、Go : およそ6秒と四倍程度遅い。スクリプト言語とは比べようもなく速いのですが。

しかしGoで並行処理をやめて(関数の先頭にgoを付けない)シングルスレッドにすると1.3秒程度で処理完了するから、並行処理にするとオーバーヘッドの分遅くなるだけです。実はGoはシングルスレッド(ネーティブで)でもマルチコアで並列処理を実行するようですね。

—————————————————

さらに以下のコードをM1 MacとRaspberry PI B+(700MHz single core)で実行速度を比較(実行速度向上の点からはほぼ無意味な並行処理)すると、

おおよそラズパイは1/150の実行速度、

 

 

admin