GolangでUDPサーバー&クライアントを作成してWiresharkで検証する
Goならわかるシステムプログラミング第7章 UDPソケットを使ったマルチキャスト通信に書かれている内容をWiresharkを使ったり、デバッグでライブラリ(netパッケージ中心)のソースを読んだりして検証してみました
サーバー側
- net.ListenPacket()の戻り値のnet.PacketConnインターフェースを使う
package main import ( "fmt" "net" ) func main() { fmt.Println("Server is runnign at localhost:8888") conn, err := net.ListenPacket("udp", "localhost:8888") if err != nil { panic(err) } defer conn.Close() buffer := make([]byte, 1500) for { length, remoteAddress, err := conn.ReadFrom(buffer) if err != nil { panic(err) } fmt.Printf("Received from %v: %v\n",remoteAddress, string(buffer[:length])) _, err = conn.WriteTo([]byte("Hello from Server"), remoteAddress) if err != nil { panic(err) } } }
ReadFrom関数の実装が気になったので確認してみた
//udpsock.go func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) { if !c.ok() { return 0, nil, syscall.EINVAL } n, addr, err := c.readFrom(b) if err != nil { err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } if addr == nil { return n, nil, err } return n, addr, err }
//udpsock_posix.go func (c *UDPConn) readFrom(b []byte) (int, *UDPAddr, error) { var addr *UDPAddr n, sa, err := c.fd.readFrom(b) switch sa := sa.(type) { case *syscall.SockaddrInet4: addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port} case *syscall.SockaddrInet6: addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port, Zone: zoneCache.name(int(sa.ZoneId))} } return n, addr, err }
//fd_unix.go func (fd *netFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) { n, sa, err = fd.pfd.ReadFrom(p) runtime.KeepAlive(fd) return n, sa, wrapSyscallError("recvfrom", err) }
//syscall_unix.go func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error) { var rsa RawSockaddrAny var len _Socklen = SizeofSockaddrAny if n, err = recvfrom(fd, p, flags, &rsa, &len); err != nil { return } if rsa.Addr.Family != AF_UNSPEC { from, err = anyToSockaddr(&rsa) } return }
//zsyscall_darwin_amd64.go // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT func recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) { var _p0 unsafe.Pointer if len(p) > 0 { _p0 = unsafe.Pointer(&p[0]) } else { _p0 = unsafe.Pointer(&_zero) } r0, _, e1 := Syscall6(SYS_RECVFROM, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(flags), uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(fromlen))) n = int(r0) if e1 != 0 { err = errnoErr(e1) } return }
このSyscall6
の中身は見れなかった。名前的にもこれはネットワーク系のシステムコールなのでしょうか。
という訳で、コールスタックは私の場合は
syscall.recvfrom (/usr/local/go/src/syscall/zsyscall_darwin_amd64.go:146) syscall.Recvfrom (/usr/local/go/src/syscall/syscall_unix.go:262) internal/poll.(*FD).ReadFrom (/usr/local/go/src/internal/poll/fd_unix.go:215) net.(*netFD).readFrom (/usr/local/go/src/net/fd_unix.go:208) net.(*UDPConn).readFrom (/usr/local/go/src/net/udpsock_posix.go:47) net.(*UDPConn).ReadFrom (/usr/local/go/src/net/udpsock.go:121) main.main (/User/Study/go-sys/chap7/udp_server/udpServer.go:18)
となった。
クライアント側
package main import ( "fmt" "net" ) func main() { conn, err := net.Dial("udp4", "localhost:8888") if err != nil { panic(err) } defer conn.Close() fmt.Println("Sending to server") _, err = conn.Write([]byte("Hello from client")) if err != nil { panic(err) } fmt.Println("Receiving from server") buffer := make([]byte, 1500) length, err := conn.Read(buffer) if err != nil { panic(err) } fmt.Printf("Received: %s\n", string(buffer[:length])) }
net.Dial
でnet.Connインターフェース(実装はnet.UDPConn)を取得してそちらにWrite,Readで送受信ができる。
とてもシンプルですね。
キャプチャ結果
この2つのソースで送受信それぞれ1つづつしかパケットが飛んでいない。
UDPはとてもシンプルですが、TCPが信頼性を担保するためにいかに頑張ってくれているか、というのが実感できるのではないでしょうか。
- 作者: 渋川よしき
- 出版社/メーカー: Lambda Note
- 発売日: 2017/10/19
- メディア: テキスト
- この商品を含むブログを見る
Golangで書かれたWebサーバーでHTTP/1.1のkeep-aliveを検証する
HTTP/1.1にはKeep-Aliveという通信方法があります。HTTP/1.0の頃は1セットの通信が終わるたびに接続・切断処理が入っていたので非効率だったため、複数のリクエストが来た場合にコネクションを使い回す機能になります。 今回はGolangで書かれたサーバーを実行してWiresharkで通信の様子をキャプチャしています
コード
keep-aliveの実装コードの一部です。Requestを終わりまで読み取り、コネクションを再利用します。
//Accept後のソケットでなんども応答を返すためにループ for { //タイムアウトを設定 conn.SetReadDeadline(time.Now().Add(5 * time.Second)) //リクエストを読み込む request, err := http.ReadRequest(bufio.NewReader(conn)) if err != nil { //タイムアウトもしくはソケットクローズ時は終了 //それ以外はエラーにする neterr, ok := err.(net.Error) //ダウンキャスト if ok && neterr.Timeout() { fmt.Println("timeout") break } else if err == io.EOF { break } panic(err) } //リクエストを表示 dump, err := httputil.DumpRequest(request, true) if err != nil { panic(err) } fmt.Println(string(dump)) content := "Hello World\n" //レスポンスを書き込む //HTTP/1.1かつ,ContentLengthの設定が必要 response := http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, ContentLength: int64(len(content)), Body: ioutil.NopCloser( strings.NewReader(content)), } response.Write(conn) }
通信内容
curlで2回リクエストを投げています。成功すると2回hello world
が帰ってきます
$ curl localhost:8888 localhost:8888 hello world hello world
キャプチャ結果
続いてWiresharkのキャプチャ結果を貼りますちょっと分かり辛いかもしれませんが、
keep aliveが無い方は2回のリクエストに対してそれぞれ接続、切断処理を行なっていますが、keep aliveがある方はコネクションを使いまわして全部で1度しか接続、切断処理をしていないことがわかります。
Golangで作成したWEBサーバーのTCP通信(HTTP GET)をWiresharkで検証
ソースコード
サーバー側のソースコードです8888ポートでクライアントからの送信を待ち受けます
package main import ( "bufio" "fmt" "io/ioutil" "net" "net/http" "net/http/httputil" "strings" ) func main() { listener, err := net.Listen("tcp", "localhost:8888") if err != nil { panic(err) } fmt.Println("Server is running at localhost:8888") for { conn, err := listener.Accept() if err != nil { panic(err) } go func() { //fmt.Printf("Accept %v\n",conn.RemoteAddr()) //リクエストを読みこむ request, err := http.ReadRequest( bufio.NewReader(conn)) if err != nil { panic(err) } dump, err := httputil.DumpRequest(request, true) if err != nil { panic(err) } fmt.Println(string(dump)) //レスポンスを書き込む response := http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 0, Body: ioutil.NopCloser( strings.NewReader("hello world\n")), } response.Write(conn) conn.Close() }() } }
クライアントの方は後述のcurlを利用します
通信方法
サーバーの方で8888のポートで受信を受け付けているのでこちらにcurlで通信を試みています
$ curl localhost:8888 hello world
サーバーの方でリクエストを受け取るとhello world
という文字列を返すようにしているのでコマンドを実行するとこちらの文字列が帰ります
localhostに対する送受信をキャプチャする準備
こちらの記事を参考にLoopback:Io0を選択しています
netstatコマンドを利用したTCPコネクションの状態
以下のタイミングでnetstatコマンドを実行してコネクションの状態を確認しました
サーバー起動時
サーバーは起動したタイミングではnetstat
コマンドの出力に8888portの情報は含まれていません。
listner.Accept()メソッドに置いたブレイクポイントで処理を止めたタイミング
tcp4 78 0 localhost.ddi-tcp-1 localhost.53315 ESTABLISHED tcp4 0 0 localhost.53315 localhost.ddi-tcp-1 ESTABLISHED
クライアント側のポート番号として53315が選ばれていることがわかります
なおこの状態でパケットをキャプチャすると以下の情報が得られました。 サーバーがGETを受け取ってOKを返す直前という感じです。
接続終了時
netstatコマンドにport8888の出力は含まれませんでした
次の記事でHTTP/1.1のKeep-Aliveを検証するのでこのnetstatの情報の差分は興味深い結果になると思います
キャプチャ結果
Wiresharkを起動しフィルタをtcp.port==8888
に設定して通信内容を記録しました。すると下のような通信内容が得られました。
解説
- 色枠1のところでTCPの3way HandShakeをしています
- 色枠2のところでデータを送受信しています
- 色枠3のところで切断処理をしています
3Way HandShake
TCPの接続開始を示すこの3つのパケットのやりとりを3 Way HandShakeと言います。
データ送受信
- データ送信直後に、受信側が送信するパケットのACKがデータを受け取った分だけ増えていることが確認できます。
- データ受信確認の後、送信側から送ったもので受信が確認できたオクテット数だけSEQが増えていることが確認できます
切断
サーバーから送ったFINのパケットに対してクライアントもFINのパケットを送り返しています。 こちらは確認応答のたびにACKを+1しています
感想
キャプチャ結果冒頭*1の[SYN]パケットを送った後の[RST,ACK]が送り返されて再度SYNパケットを送信している理由がわかりませんでした。 一般的な挙動には見えないのですが。。。これはGoのListnerの実装などを読めばわかるのでしょうか。次回はKeep-Aliveを実験してみたいと思います
参考
*1:①の上に位置する2つのパケット
Wiresharkでlocalhost(127.0.0.1)宛のパケット送受信をキャプチャする方法
「Goならわかるシステムプログラミング」のネットワークプログラミングの章でプログラムの検証をやりたかったのでLoopbackアドレスに対するパケット送受信ができないのか調べてみたところ、日本語ではあまりズバリな解決策*1が出なかったため英語で調べると解決した。
「wireshark how to capture localhost」でググったところ以下の投稿を参考にwiresharkのwikiにたどり着いた
https://stackoverflow.com/questions/5847168/wireshark-localhost-traffic-capture
https://wiki.wireshark.org/CaptureSetup/Loopback
Summary: you can capture on the loopback interface on Linux, on various BSDs including Mac OS X, and on Digital/Tru64 UNIX, and you might be able to do it on Irix and AIX, but you definitely cannot do so on Solaris, HP-UX, or Windows.
上記引用にある通り、WiresharkのNIC選択一覧に出ているものの中には、loopbackのパケットをキャプチャできるものがある。
今回はこちらを選択することで無事localhost宛のパケットをキャプチャすることができた
ただしmacOSではこの方法が使えたが
では使えないので該当OSユーザーの方は別の方法を模索してください
*1:ルーティングテーブルを書き換えろとか、別のツールを入れろとか
Effective SQL 読書会(5)に参加
参加者が少なかったので木村さんの話がいつもより多く聞けました。 サポートやコンサル、DBMSの制作などをされている方なのでその膨大な知見に毎度驚かされます。
今回は
- (続き)第2章 プログラム可能性とインデックスの設計
- 項目17 計算値をインデックスで使用する状況を理解する
- 第3章 設計を変更できないときはどうするか
- 項目18 変更できないものはビューを使って単純化する
- 項目19 ETLを使って非リレーショナルデータを情報に変える
- 項目20 サマリテーブルを作成して管理する
を読みました
- 作者: John L. Viescas,Douglas J. Steele,Ben G. Clothier,株式会社クイープ
- 出版社/メーカー: 翔泳社
- 発売日: 2017/12/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (3件) を見る
過去のSQLServerではNULLが値として扱われていた。
そのためNULL is NULL
じゃなくてNULL = NULL
でもTrue
となる
ANSI_NULLSオプションをオンにするとこのようなおかしな挙動を防げる
最近だとデフォルトで有効化されているので安心。
- MySqlのTIMESTANP型はタイムゾーンを考慮する
- 関数ベースのインデックスは決定関数でしか使えない
- 関数か決定的かどうかは関数定義時に特殊なキーワードを使う
- DETERMINISTIC句(Oracle)
- IMMUTABLE句(PostgreSQL)
- その関数が決定的かどうかはコンパイル時にわからない。
- インデックスに利用する関数が決定的であることはプログラマが責任を持たなければならない。
- SQLliteの専門的な知見を持っている日本人はかなり少ない
- らしいです
- ETLツール
- 静的なデータファイルをDBにインポートするツール
- それらしいものは何度か触ったことがあるが、ETL(Extract, Transform, Load)という名称は知らなかった
- 商用のものだとData Spiderなんかも有名
- PostgreSQLではTALEND(OSS)なんかが利用可能
- 計算量の大きいデータをキャッシュする仕組み
- マテリアライズドビュー
- サマリーテーブル
- サマリーテーブルは有用だがあまり活用されないらしい
MySQLのイベントスケジューラー
- https://dev.mysql.com/doc/refman/5.6/ja/events.html
- Oracleにもある
- 定期的なインデックスのリビルドで利用できる
ラズパイは正直、あまり大規模ではない組み込み用途か教育用でしか利用できないと思っていたが、 分散システムのようなものを作ってパフォーマンスがシビアな状況でも利用しているらしい
次回は「項目21 UNIONを使って非正規化データをアンピボットする」から
プログラマのためのSQL 読書会(26)に参加
今回もやたら難解なクエリが多く、輪読がはかどらず、もくもく会になってしまう場面が多かったです*1
- 作者: ジョー・セルコ,Joe Celko,ミック
- 出版社/メーカー: 翔泳社
- 発売日: 2013/05/24
- メディア: 大型本
- この商品を含むブログ (16件) を見る
検証環境:
- MySQL:
Ver 14.14 Distrib 5.7.22, for osx10.13 (x86_64) using EditLine wrapper
- PostgreSQL:
PostgreSQL 10.1 on x86_64-apple-darwin13.4.0
FETCH FIRST ROWS
fetch first 3 rows
を使って結果から「N行までのデータ」という取り方をすることができます。
それぞれのDBMSの実装依存でTOPやLIMITなども使えますが、一応SQL標準(ANSI 2008)の行数制限指定はこちらですので、
標準SQLにこだわる方はこちらの存在を知っておいた方が良いです。
list_seq | cat_string ----------+------------ 1 | abc 2 | bcd 3 | cde 4 | def 5 | efg 6 | fgh 7 | ghi
SELECT * FROM make_string FETCH FIRST 3 ROWS ONLY;
list_seq | cat_string ----------+------------ 1 | abc 2 | bcd 3 | cde (3 rows)
limit句とfetch first句の対応状況です。
SQL-対応DB | Oracle | MySQL | PostgreSQL | DB2 | SQLServer |
---|---|---|---|---|---|
limit | ◯ | ◯ | ◯ | × | × |
fetch first | ◯ | ◯ | ◯ | ◯ | ◯ |
SQLServerでは標準化される前はTOPという構文を使っていたようです。
p.547のクエリが動くのはPostgresだけ
らしいです
--▼P.547 CREATE TABLE Make_String (list_seq INTEGER NOT NULL PRIMARY KEY, cat_string VARCHAR NOT NULL);--MySQLで試す場合はVARCHARのサイズ指定が必要 --- INSERT INTO Make_String VALUES (1, 'abc'), (2, 'bcd'), (3, 'cde'), (4, 'def'), (5, 'efg'), (6, 'fgh'), (7, 'ghi'); --- WITH RECURSIVE String_Tail(list_seq, cat_string) AS (SELECT list_seq, cat_string FROM Make_String WHERE list_seq > 1), String_Head(list, max_list_seq, list_seq) AS (SELECT cat_string, 1, list_seq FROM Make_String UNION ALL SELECT String_Head.list || ', ' || String_Tail.cat_string, String_Head.max_list_seq + 1, String_Head.list_seq FROM String_Tail, String_Head WHERE String_Tail.list_seq = String_Head.max_list_seq + 1) SELECT list FROM String_Head WHERE list_seq = 1 AND max_list_seq = (SELECT MAX(list_seq) FROM Make_String);
Postgresで試した結果
list ----------------------------------- abc, bcd, cde, def, efg, fgh, ghi (1 row)
確かにMySQLではエラーになった。
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'RECURSIVE String_Tail(list_seq, cat_string) AS (SELECT list_seq, cat_string F' at line 1
p551のクエリに関して
CREATE TABLE Performance (portfolio_id CHAR(7) NOT NULL, execute_date DATE NOT NULL, rate_of_return DECIMAL(13,7) NOT NULL); --- INSERT INTO Performance VALUES ('001', '2006-01-06', 0.5), ('001', '2006-01-07', 0.3), ('001', '2006-01-08', 0.6), ('001', '2006-01-09', 0.8), ('001', '2006-01-10', 0.1); --- CREATE TABLE BigPi (execute_date DATE NOT NULL, day_1 INTEGER NOT NULL, day_2 INTEGER NOT NULL, day_3 INTEGER NOT NULL, day_4 INTEGER NOT NULL, day_5 INTEGER NOT NULL); --- INSERT INTO BigPi VALUES ('2006-01-06', 1, 0, 0, 0, 0), ('2006-01-07', 0, 1, 0, 0, 0), ('2006-01-08', 0, 0, 1, 0, 0), ('2006-01-09', 0, 0, 0, 1, 0), ('2006-01-10', 0, 0, 0, 0, 1);
SELECT portfolio_id, (SUM((1.00 + P1.rate_of_return) * M1.day_1) * SUM((1.00 + P1.rate_of_return) * M1.day_2) * SUM((1.00 + P1.rate_of_return) * M1.day_3) * SUM((1.00 + P1.rate_of_return) * M1.day_4) * SUM((1.00 + P1.rate_of_return) * M1.day_5)) AS product FROM Performance AS P1, BigPi AS M1 WHERE M1.execute_date = P1.execute_date AND P1.execute_date BETWEEN '2006-01-06' AND '2006-01-10' GROUP BY portfolio_id;
途中結果がsumを経由しているので値として0になっているのか、nullが含まれているから0担っているのかわからない、という問題があり対策のCASE式として以下のものが書籍中に紹介されていた
CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_N)=0.00 THEN CAST(NULL AS DECIMAL(13,7)) ELSE SUM((1.00 + P1.rate_of_return) * M1.day_N) END
この対策は結局P1.rate_of_return
に-1のものが入ればNULLが伝搬して結果がNULLになってしまうのでは?ということを主張しました(が検証しきれなかった)
上記を適用したクエリと全体は以下のようになる
SELECT portfolio_id, ( CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_1)=0.00 THEN CAST(NULL AS DECIMAL(13,7)) ELSE SUM((1.00 + P1.rate_of_return) * M1.day_1) END * CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_2)=0.00 THEN CAST(NULL AS DECIMAL(13,7)) ELSE SUM((1.00 + P1.rate_of_return) * M1.day_2) END * CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_3)=0.00 THEN CAST(NULL AS DECIMAL(13,7)) ELSE SUM((1.00 + P1.rate_of_return) * M1.day_3) END * CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_4)=0.00 THEN CAST(NULL AS DECIMAL(13,7)) ELSE SUM((1.00 + P1.rate_of_return) * M1.day_4) END * CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_5)=0.00 THEN CAST(NULL AS DECIMAL(13,7)) ELSE SUM((1.00 + P1.rate_of_return) * M1.day_5) END ) AS product FROM Performance AS P1, BigPi AS M1 WHERE M1.execute_date = P1.execute_date AND P1.execute_date BETWEEN '2006-01-06' AND '2006-01-10' GROUP BY portfolio_id;
とりあえず実行してみたところ結果は変わらない
portfolio_id | product -------------+--------------------------------------- 001 | 6.17760000000000000000000000000000000 (1 row)
適当にPerformanceテーブルからレコードを抜くとnullになった
portfolio_id | product -------------+--------- 001 | (1 row)
もちろん対策前のクエリでは結果は0になります
portfolio_id | product -------------+--------------------------------------- 001 | 0.00000000000000000000000000000000000 (1 row)
結局
P1.rate_of_return
に-1のものが入ればNULLが伝搬して結果がNULLになってしまうのでは?
上記の自分の疑問が検証できそうなデータを作成しました
DELETE FROM Performance; INSERT INTO Performance VALUES ('001', '2006-01-06', 0.5), ('001', '2006-01-07', 0.3), ('001', '2006-01-08', 0.6), ('001', '2006-01-09', -1),--これ! ('001', '2006-01-10', 0.1);
するとやはり結果はNULLになりました
portfolio_id | product --------------+--------- 001 | (1 row)
ただし利率が-1になるような金融というか口座は存在し得ないと思うのでこちらの不具合は多分起こらないかも。 というか普通はこの辺の処理はホスト側の言語でやると思います。
感想
特に本書中で触れられていませんが、BigPi
の中身のデータのような感じで
単位行列のようなデータを利用した変態なクエリが多かった印象です。
セルコの他の本でグラフ理論?を扱っているものがありましたが、 線形代数(行列)を使ったクエリ、テーブル設計、RDBMSの実装などもあればさらに変な使い方ができて面白いかも知れません。
*1:おそらく自分が出たことのある会で一番進捗が悪かったと思います
第20回横浜Go読書会に参加
初参加です。
- 作者: 渋川よしき
- 出版社/メーカー: Lambda Note
- 発売日: 2017/10/19
- メディア: テキスト
- この商品を含むブログを見る
Goを学び始めた動機などは
にも書いていますが、インタプリタの本でGoのコードをちょっとみたのが大きかったかなと思います。*1
結局先日からは章末の連取問題を解くのに必死で全然読み進められていません。(p146で止まっている) 会場に向かう電車の中で軽く流し読みをして挑みました。
今回は13章のGo言語と並列処理の240ページからでした
Go1.11
Go1.11が近日発表された、ということで、リリースノートを軽く読みました。
Webassemblyサポート
サンプルを試したところ、ChromeのコンソールにHello Worldが出せたらしい
Goのデバッガ
最適化したコードでもデバッグ情報がかなり出るようになった
Go fmtが少々変わる
古いバージョンのGoで静的解析が通っていたソースが 今回のバージョンでフォーマットの違いで警告が出る恐れがある
- CIのバージョンが違うと怒られる可能性がある
Go2
まだ仕様は出ていないが、軽くディスカッションされている
- generics
- エラー処理に関して重要な変更が2件
Go 1.12は半年後くらい。 いつ出るかわからない
mutexはgorutin間で通信する時に重要
書き込みが1つだけであっても、 他のスレッド(gorutin)から「書いた」値を読めるように書き込みの完了までをロックする必要がある。 *2*3
メモリモデルは
https://golang.org/ref/mem#tmp_5
が詳しい
重い処理をマルチスレッドで実行する場合はCPUの物理的な個数分のプロセスに分解するとパフォーマンスが良い
Goではruntime.NumCPU()
でCPUの個数を取得できるが、こちらで確認できるのは物理CPUの数ではなく、
論理CPU の数であるため注いが必要。
本書に記載されている通り、SMT(Simultaneous Multi-Threading)機能などにより、CPUのコア数が多く評価されることがある。
自分の環境で試したところ、デュアルコアであるにも関わらず、4が帰ってきた。
Goでは並列を保証しない。
平行だけ。これは各CPUに割り当てるプロセス「プロセス」は作れても 空いてる新規にプロセスを割り当てるCPUを増やすなどの動的な制御をGo内だけで処理できない。
実行じにGOMAXPROCの環境変数で実行時に指定するか、OSのスケジューラによる割り当てに任せる。
p242で配列が1000万件あったら,という話題になった
package main import ( "fmt" "time" ) func main() { tasks := []string{ "cmake ..", "cmake. --build Release", "cpack", } for _, task := range tasks { go func() { fmt.Println(task) }() } time.Sleep(time.Second) }
- 上記のコードは動機をとっていないので最初のgoroutineの実行時にfor文が周りきった状態になり最後の要素が3回出力される、という悪い見本コードです.
- ちなみに動機を
Sleep
を使って取るのもあまり良い手本ではありません
- ちなみに動機を
- 流石に1万もあれば最後ではなく、最初の要素が出るでしょう、という話になった
- 結局はスケジューラーによるのでは?
実験してみました。まずはサンプルコードのまま3件でそのまま動かしました。
cpack cpack cpack
本書の記載通り、最後の要素であるcpack
が帰ってきました
ではsliceの中身を100個に増やしてみましょう
package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(100) tasks := []string{ "1", "2", "3", "4", "5", "6", ...(略) "99", "100", } for _, task := range tasks { go func() { fmt.Println(task) wg.Done() }() } wg.Wait() }
このコードで実験してみました
注:前のコードでmain終了を待たせる同期を取っている箇所、time.Sleep(time.Second)の期限で処理が完了しなかったのでWaitGroupを使っています*4
結果
13 6 30 31 41 10 54 54 54 54 54 54 54 54 46 54 54 54 54 62 68 68 68 68 68 68 68 68 78 78 60 62 68 68 68 54 68 70 78 68 78 78 78 78 78 62 78 78 78 78 78 100 100 100 100 78 100 100 78 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 100 73 100
という出力が得られました。
一番最初/最後の要素が必ず出るという訳ではなさそうです。 結局は内部でどのようにスケジューリングしているか、という話なのでしょうか。
sync/atomicパッケージはgoで書かれていない
このあたりのコードは全てアセンブリで描かれている。
CPUごとに処理をさせたいときはCounting semaphoreを使う
Wikipediaによると、
任意個の資源を扱うセマフォをカウンティングセマフォ、値が0と1に制限されている(ロック/アンロック、使用可能/使用不可の意味がある)セマフォをバイナリセマフォと呼ぶ。後者はミューテックスと同等の機能を持つ。
とのことです
https://ja.wikipedia.org/wiki/セマフォ
セマフォは大学の時にOSの単元で軽く学習しましたが、 あまり覚えておらず、他の分散系の読書会でも出てきたので、どこかで復習しておきたいと思います。
interface{}はVariant型
読んでいるコードのところでわからない箇所があって助け舟を出してもらったりしました。
var count int pool := sync.Pool{ New: func() interface{} { count++ return fmt.Sprintf("created:%d", count) }, } pool.Put("manualy added: 1") pool.Put("manualy added: 2") fmt.Println(pool.Get())//manualy added: 1 fmt.Println(pool.Get())//manualy added: 2 fmt.Println(pool.Get())//created:1
poolに中身がないとNew:~
以降が実行されてcountがインクリメントされた後string型が返されます。
interface{}の部分をStringに変えたり色々試してみましたが、まだこの部分の文法が何をやっているのかよくわかりませんでした。
この辺は時間をかけてGoを学んで吸収したいと思います
感想
Javaの有名な本を沢山訳されていたり、大きな講演会の基調講演をされている柴田さんがやられている勉強会なので、 前から気になっていましたが、敷居が高いかなと思い参加せずにいましたが、この読書会はそこまでハードコアではないと耳にしたので*5、思い切って参加してみました。
今回の範囲ですが、GoはランタイムがOSのような資源管理をしているのが面白いと思いました。
柴田さんが懇親会で「若い人はまともなAPI設計ができない、経験が積めていない」という趣旨のことをなんども仰られていました。
APIデザインの極意 Java/NetBeansアーキテクト探究ノート
- 作者: Jaroslav Tulach
- 出版社/メーカー: インプレス
- 発売日: 2014/07/02
- メディア: Kindle版
- この商品を含むブログ (8件) を見る
柴田さんが訳されたAPI設計の極意、10%ほど読んで放置になっていたので、こちらも早いうちに読まないといけないと思います。
パタヘネとコンピュータネットワーク(セスぺの試験までには読み終わりたいがこちらもペース的に若干きつい)も平行して読みたいので、勉強することが多くて大変だなと思います。