Effective Go 野良翻訳(4)
これでおわり。並行性、エラー、Webサーバ。
並行性
通信による共有
並行プログラミングは大きなトピックだ。スペースの関係上、Go固有の特徴的な話だけに絞る。
多くの環境において並行プログラミングが大変なのは、共有される変数へのアクセスを巧妙に正しく実装しなければならないからだ。Goでは異なるアプローチを推奨する。共有する値はチャネルで引き渡すので、実際には複数の実行スレッドが値を共有することはない。ある瞬間にある値にアクセスするgoroutineは常に1つだ。データ競合は、設計上けして生じない。この考え方を推奨するためにスローガンにしてみた。
メモリを共有することによって通信してはいけない。通信によってメモリを共有するのだ。
このアプローチではやり過ぎになる場合もある。たとえば、参照カウントなどは、整数の変数の周りを排他ロックで囲んで実装するのがベストだろう。しかし、高レベルのアプローチとしては、チャンネルを用いてアクセスを制御したほうが、明快で正しいプログラムを簡単に書ける。
次のように考えてみよう。まず、1CPUで動作する典型的なシングルスレッドのプログラムを考える。これには同期プリミティブは必要ない。次に、同じようなプログラムがもう一つあるとしよう。ここでも同期は必要ない。さて、この二つのプログラムを通信させてみよう。通信が同期機構になっていれば、他の同期は必要ない。たとえばUnixのパイプはこのモデルに完全に合致する。Goの並行性へのアプローチはHoareのCSP(Communicating Sequential Processes)に由来するのだが、型安全に拡張されたUnixパイプだと考えることもできる。
Goroutine
Goではgoroutineという言葉を使う。既存の言葉、たとえばスレッド、コルーチン、プロセスなどには不正確な暗黙の意味が付随するからだ。goroutineのモデルは簡単だ。同じアドレス空間で他のgoroutineと並列に動作する関数、だ。軽量で、スタック空間の確保+α程度しかコストがかからない。スタック空間は最初は小さく確保されるので軽量だ。スタックは必要に応じてヒープを確保して伸張する。
goroutineは、たとえばI/O待ちで1つのgoroutineがブロックしたような場合に、他のgoroutineが実行を続けられるように、複数のOSスレッドで実行される。このデザインによって、面倒なスレッドの生成と管理が隠蔽される。
関数やメソッドの呼び出しに go というキーワードをつけるだけで、その呼び出しが新しいgoroutineで実行されるようになる。呼び出しが終了すると、goroutineは静かに終了する(Unixのシェルで &をつけるとコマンドがバックグラウンドで実行されるのに似ている)。
go list.Sort() // 並行にlist.Sortを呼び出す。終了を待たない。
関数リテラルを使うとgoroutineの呼び出しが簡単になる。
func Announce(message string, delay int64) { go func() { time.Sleep(delay) fmt.Println(message) }() // この括弧に注意。関数をよびださなければならない。 }
Goの関数リテラルはクロージャだ。関数が参照している変数は、その関数がアクティブな間は開放されないことが保証されている。
上の例では、関数が終了を告知する方法がないのであまり実用的とは言えない。そのために次のチャンネルが必要なのだ。
チャンネル
マップと同様に、チャンネルは、参照型で、makeで確保する。省略可能なint型パラメータが与えられると、その値がチャンネルのバッファサイズになる。デフォルトはゼロで、この場合はバッファなし、すなわち同期型のチャンネルになる。
ci := make(chan int) // int型バッファなしチャンネル cj := make(chan int, 0) // int型バッファなしチャンネル cs := make(chan *os.File, 100) // Fileへのポインタ型バッファ付きチャンネル
チャンネルは、通信(値のやりとり)と同期(二つの計算(goroutine)が既知の状態になっていることを保証する)を合わせたものだ。
チャネルにはさまざまな使い方がある。まずは次の例を見てみよう。前節の例では、ソートをバックグラウンドで実行していた。チャンネルをつかえばソートが終了するのを待つことができる。
c := make(chan int) // チャンネルの確保 // goroutineでソートを実行。終了したらチャンネルにシグナルを送る go func() { list.Sort() c <- 1 // シグナル送信。値はどうでもいい。 }() doSomethingForAWhile() <-c // ソートの終了を待つ。送られてきた値は捨てる。
受信者は、常に値をうけとるまでブロックする。バッファなしチャンネルの場合には、送信者も受信者が値を受け取るまでブロックする。バッファ付きチャンネルの場合には、送信者は、送信値がバッファにコピーされるあいだだけブロックする。バッファがフルだった場合には、どこかの受信者が値を受信するまでブロックすることになる。
バッファ付きチャンネルはセマフォのように、スループットを制限するために使うことができる。次の例では、入ってきたリクエストはhandleに渡される。handleでは、値をチャンネルに書き込み、リクエストを処理し、また値をチャンネルから読み出している。チャンネルのバッファサイズが、processが同時に呼び出される数を制限する。
var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // アクティブキューが終了するのを待つ process(r) // 時間のかかる処理 <-sem // 終了。次のリクエストが処理できるようにする } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // handleの終了を待たない } }
同じことを、固定数のgoroutineが一つのリクエストチャンネルから読み出すようにして実装した例を示す。goroutineの数が、同時に呼び出されるprocessの数を制限する。このServe関数は終了シグナルを受け付けるチャンネルを引数として取る。この関数はgoroutineを起動しおわると、そのチャンネルの読み出しでブロックする。
func handle(queue chan *Request) { for r := range queue { process(r) } } func Serve(clientRequests chan *clientRequests, quit chan bool) { // Start handlers for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // Wait to be told to exit. }
チャンネルのチャンネル
Go言語の最も重要な性質の一つは、チャンネルがファーストクラスの値であり、他の値と同様に確保し受け渡すことのできるものになっていることだ。この特性を利用して、安全な並列実行ができる。
前節で示した例では、handle は理想化されたリクエストハンドラで、処理するデータ型については定義していなかった。処理するリクエストに答えを返すためのチャンネルを持たせれば、クライアントがそこにチャンネルをセットして、そこから答えを取り出すようにすることができる。Request 型の骨組はこんな感じになる。
type Request struct { args []int f func([]int) int resultChan chan int }
クライアントは、リクエストオブジェクトの中に、関数と引数に加えて
答えを返却するためのチャンネルをいれる。
func sum(a []int) (s int) { for _, v := range a { s += v } return } request := &Request{[]int{3, 4, 5}, sum, make(chan int)} // リクエスト送信 clientRequests <- request // レスポンスを待つ fmt.Printf("answer: %d\n", <-request.resultChan)
サーバ側では、ハンドラ関数だけ変更すればいい。
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) } }
実用にするにはもっといろいろしなければならないのは明らかではあるが、このコードはちゃんとした、実行レートが制限された並列ノンブロッキングRPCシステムになっている。しかも、排他制御は全く使われていない。
並列実行
同じアイディアの応用として、計算を複数のCPUコアで並列に実行することが考えられる。計算が複数の独立した部分に分割できるのであれば、並列化することができる。部分の終了はチャンネルでシグナルする。
Vectorの各要素に対して重い計算を行うことを考えてみよう。ここでは理想的に考えて、計算の処理は独立にできることとする。
type Vector []float64 // Apply the operation to v[i], v[i+1] ... up to v[n-1]. func (v Vector) DoSome(i, n int, u Vector, c chan int) { for ; i < n; i++ { v[i] += u.Op(v[i]) } c <- 1 // signal that this piece is done }
この部品をループで独立に、1CPUあたり1つ起動する。どの順番で終了するかはわからないが、あまり関係ない。
すべてのgoroutineを起動したら、あとは、チャンネルからの終了信号を捨てながら数えるだけだ。
const NCPU = 4 // number of CPU cores func (v Vector) DoAll(u Vector) { c := make(chan int, NCPU) // Buffering optional but sensible. for i := 0; i < NCPU; i++ { go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c) } // チャンネルから読み出して捨てる for i := 0; i < NCPU; i++ { <-c // wait for one task to complete } // All done. }
現在のgc (6gなど)の実装では、デフォルトではこのコードを自動的に並列化しない。ユーザレベルの処理には、1コアだけを使う。任意個のgoroutineがシステムコールでブロックすることができるが、デフォルトでは、任意の瞬間にユーザレベルコードを実行しているgoroutineは1つだけだ。CPU並列実行をするには、同時に実行したいgoroutineの数をランタイムシステムに教えてやらなければならない。もっといい方法がある、いつかはそうなるだろうが、それまでは。これには2つの方法がある。環境変数GOMAXPROCSを使用するコア数(デフォルトは1)にセットする、もしくは、runtimeパッケージをインポートしてruntime.GOMAXPROCS(NCPU)を呼び出す。もう一度書いておくが、スケジューリングとランタイムシステムが発展すれば、このような指定をする必要は無くなるはずだ。
リークするバッファ
この、並行プログラミングのための道具は、並行とは関係のない概念を実装するためにも利用できる。ここで示すのは、RPCパッケージから取ってきて抽象化したコード例だ。クライアントのgoroutineはどこか、おそらくはネットワークからデータを受け取るループを回っている。バッファを確保したり開放したりするのを避けるために、フリーリストを使っている。ここで、バッファ付きのチャンネルを使っているのだ。チャンネルが空だったら、新しいバッファが確保される。バッファにデータを納めたら、serverChanからサーバに送る。
var freeList = make(chan *Buffer, 100) var serverChan = make(chan *Buffer) func client() { for { b, ok := <-freeList // grab a buffer if available if !ok { // if not, allocate a new one b = new(Buffer) } load(b) // read next message from the net serverChan <- b // send to server } }
サーバループは、クライアントからメッセージを受け取って処理し、バッファをフリーリストにもどす。
func server() { for { b := <-serverChan // wait for work process(b) _ = freeList <- b // reuse buffer if room } }
クライアントは、freeListに対するノンブロッキング受信を行い、フリーリストにバッファがあれば、バッファを取り出す。バッファがなければ、新しいのを確保する。サーバは、freeListに対するノンブロック送信を行い、フリーリストがフルでなければ、バッファbをフリーリストに戻す。バッファがフルだった場合にはバッファは、床の上におとされ、ガベージコレクタに回収される(送信操作をブランク識別しに代入すると、送信操作がノンブロッキングになり、その送信が成功したかどうかは無視される)。このコードはバッファ付きチャンネルとガベージコレクションによる後始末に依存して、リークするバッファフリーリストをわずか数行で実装しているのだ。
エラー
ライブラリルーチンは、さまざまなエラーが生じたことを呼び出した側に知らせなければならない。前述したとおり、Goには複数返り値があるので、通常の返り値とエラーの詳細を同時に返すことが簡単にできる。慣例としてエラーには、簡単なインターフェイスであるos.Error型を用いる。
type Error interface { String() string }
ライブラリを記述するプログラマが、このインターフェイスをベースにもっとリッチなエラーモデルを実装して、エラーそのものだけでなくエラーのコンテクストを知ることができるようにしてもよい。例えば、os.Open は次のos.PathErrorを返す。
// PathError records an error and the operation and // file path that caused it. type PathError struct { Op string // "open", "unlink", etc. Path string // The associated file. Error Error // Returned by the system call. } func (e *PathError) String() string { return e.Op + " " + e.Path + ": " + e.Error.String() }
PathErrorのString メソッドは次のような文字列を生成する。
open /etc/passwx: no such file or directory
このように、問題があったファイル名、操作、エラーを発生させたOSのシステムコールエラーが出力される。このようなエラーメッセージは、原因となった呼出から遥かに離れたところで出力された場合にも有用だ。"no such file or directory" だけのメッセージよりも、遥かに多くの情報を含んでいる。
呼び出し側がエラーの詳細を知りたければ、型switchもしくは型アサーションを用いてエラーを特定し、詳細を抽出することができる。PathErrorであれば、さらに回復可能な障害かどうかを調べることもできるだろう。
for try := 0; try < 2; try++ { file, err = os.Open(filename, os.O_RDONLY, 0) if err == nil { return } if e, ok := err.(*os.PathError); ok && e.Error == os.ENOSPC { deleteTempFiles() // Recover some space. continue } return }
Panic
エラーが起きたことを知らせる通常の方法は、os.Errorを追加の返り値として返す方法だ。標準のReadメソッドがいい例だ。このメソッドは読み出したバイト数と、os.Errorを返す。だが、エラーが回復不可能なものだったらどうしたらいいだろうか。プログラムの続行が全く不可能な場合もある。
このような時に使うために、組み込み関数 panic が用意されている。この関数は、実行時エラーを生成し、プログラムの実行を停止する(ただし、次のセクションを見よ)。この関数は、任意の型の引数を一つとる。この引数(通常は文字列を用いる)は、プログラムが死んだ際に表示される。panicは、なにかありえないことが起きたことを示すために用いる場合もある。実際、コンパイラは関数の最後にpanicが書かれていると、それを認識して通常のreturn文チェックを抑制する。
// A toy implementation of cube root using Newton's method. func CubeRoot(x float64) float64 { z := x/3 // Arbitrary intitial value for i := 0; i < 1e6; i++ { prevz := z z -= (z*z*z-x) / (3*z*z) if veryClose(z, prevz) { return z } } // A million iterations has not converged; something is wrong. panic(fmt.Sprintf("CubeRoot(%g) did not converge", x)) }
これは単なる例で、本当のライブラリではpanicは使うべきではない。問題を隠したり回避できたりする場合には、プログラム全体を落とさず、実行を続けたほうがいい。これに対する反例としては、プログラムの初期化時がある。ライブラリが初期化ができないような場合には、panicしたほうがいい場合もある。
var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }
Recover
ユーザがpanicを呼んだ場合や、配列のインデックスが範囲を外れた場合や型アサーションが失敗した場合などの実行時エラーが起きて暗黙裡にpanicが呼ばれた場合、現在の関数の実行は即座に停止され、defer関数を実行しながらのgoroutineスタックの巻き戻しが行われる。巻き戻しの過程がgoroutineのスタックのトップに到達すると、プログラムは停止する。ただし、組み込み関数recoverを用いると、panicが起こったgoroutineの制御を取り戻し、通常の実行に戻すことができる。
recoverが呼び出されると、巻き戻しが停止し、panicに渡された引数がrecoverの返り値になる。巻き戻しの過程で実行されるコードはdefer関数の中だけなので、recover はdefer関数の中でのみ有効になる。
recoverを利用するケースの例として、複数のgroutineを用いるサーバプログラムが挙げられる。あるgoroutineが失敗したとき、他の実行中のgoroutineを殺すことなく、シャットダウンすることができる。
func server(workChan <-chan *Work) { for work := range workChan { go safelyDo(work) } } func safelyDo(work *Work) { defer func() { if err := recover(); err != nil { log.Println("work failed:", err) } }() do(work) }
この例では、do(work) がpanicすると、そのことがログに書き出され、他のgoroutineに影響を与えずに、そのgoroutineだけが綺麗に終了する。defer 節ではほかに何もする必要はない。recoverを呼び出すだけで、panicを完全に処理することができる。
このリカバリパターンを用いると、do関数およびそこから呼び出される関数では、panicを呼ぶことですべてを精算できる。このアイディアをつかって、複雑なソフトウェアのエラー処理を簡単化することができる。下に示すのは、regexpパッケージの一部を単純化したものだ。ここでは、 パーズエラーをpanicをローカルなError型を引数に呼び出すことでエラーを報告している。Errorの定義とerrorメソッドとcompile関数を示す。
// Error is the type of a parse error; it satisfies os.Error. type Error string func (e Error) String() string { return string(e) } // error is a method of *Regexp that reports parsing errors by // panicking with an Error. func (regexp *Regexp) error(err string) { panic(Error(err)) } // Compile returns a parsed representation of the regular expression. func Compile(str string) (regexp *Regexp, err os.Error) { regexp = new(Regexp) // doParse will panic if there is a parse error. defer func() { if e := recover(); e != nil { regexp = nil // Clear return value. err = e.(Error) // Will re-panic if not a parse error. } }() return regexp.doParse(str), nil }
doParseがpanicした場合、recoverブロックで返り値をnilにセットする。defer 関数内で名前付き返り値の値を変更することができるのだ。次に、err変数への代入によって、生じた問題が、パーズエラーだったのかどうかをError型への型アサーションによってチェックする。Error 型でない場合には、型アサーションが失敗し、実行時エラーが発生してスタックの巻き戻しが何事もなかったかのように続行される。このチェックは、なにか本当に予期していなかったこと、例えば配列の添字エラーなどが起きた場合には、ユーザが起動したエラーに対してpanic - recoverを使っていても、きちんとコードが失敗するようにするためだ。
このようにエラー処理すると、errorメソッドで、パーズエラーを返すのが簡単になる。パーズスタックを自分で巻き戻すことを心配しなくて済むからだ。
この書き方は有用ではあるが、パッケージ内でのみ使うべきだ。このParse関数は、内部のpanic呼出をos.Errorの値に変換し、panicを呼び出し側には見せていない。このルールは守ろう。
Webサーバ
最後に一つ完全なGoのプログラムを示そう。Webサーバだ。というか、リダイレクトサーバだ。Googleは、http://chart.apis.google.com で、データをチャートやグラフに変換するサービスを提供している。しかしこのサービスでは、データをURLにクエリとして入れ込まなければならないので、インタラクティブに使うのは難しい。
このプログラムはある種類のデータに対してちょっと使いやすいインターフェイスを提供する。短いテキストを入力すると、チャートサーバを呼び出してQRコードを生成する。QRコードは格子状に並んだボックスでテキストをエンコードした画像だ。この画像は、携帯電話のカメラで撮影して、たとえばURLとして解釈することができるので、携帯電話の小さなキーボードで、URLをわざわざ入力しなくてすむ。
完全なプログラムを示す。解説はその後で。
package main import ( "flag" "http" "io" "log" "template" ) var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18 var fmap = template.FormatterMap{ "html": template.HTMLFormatter, "url+html": UrlHtmlFormatter, } var templ = template.MustParse(templateStr, fmap) func main() { flag.Parse() http.Handle("/", http.HandlerFunc(QR)) err := http.ListenAndServe(*addr, nil) if err != nil { log.Exit("ListenAndServe:", err) } } func QR(w http.ResponseWriter, req *http.Request) { templ.Execute(req.FormValue("s"), w) } func UrlHtmlFormatter(w io.Writer, v interface{}, fmt string) { template.HTMLEscape(w, []byte(http.URLEscape(v.(string)))) } const templateStr = ` <html> <head> <title>QR Link Generator</title> </head> <body> {.section @} <img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={@|url+html}" /> <br> {@|html} <br> <br> {.end} <form action="/" name=f method="GET"><input maxLength=1024 size=70 name=s value="" title="Text to QR Encode"><input type=submit value="Show QR" name=qr> </form> </body> </html> `
このプログラムの意味は簡単にわかるだろう。flagで、デフォルトのHTTPポートを設定している。template変数tmplはちょっと面白い。これは、サーバがページを表示するために利用するHTMLテンプレートを作成する。詳しい説明は後でする。
main関数は、オプションフラグを解析し、上で説明した機構を用いて、関数QRをサーバのルートパスに紐付けている。次に http.ListenAndServe を呼んでサーバを起動している。サーバが実行している間はブロックする。
関数QRはデータが収められたリクエストを受け取り、フォームのsに書かれた値に対して、テンプレートを実行する。
templateパッケージは、json-templateにインスパイアされて造られたもので、パワフルなテンプレート機能を提供する。このプログラムはその機能の一端しか用いていない。テンプレートとは、要するにテキストの要素をtempl.Executeで渡されたデータでおきかえることで、テキストを動的に書き換える機能だ。この場合、置き換える値は、フォームで入力された値だ。テンプレートテキスト(templateStr)の中の、中括弧('{}')で囲まれた部分が、テンプレートアクションを示す。{.section @} から {.end} のあいだの部分は、データアイテム@に対して実行される。@は「現在のアイテム」を示す略記法で、この場合はフォームから入力された値だ(この文字列が、空だった場合にはテンプレートのこの部分は出力されない)。
{@|url+html}という部分は、データをフォーマッタマップ(fmap)の"url+html"にインストールされたフォーマッタでデータを処理しろという意味だ。この場合フォーマッタはUrlHtmlFormatterで、Webページ上に安全に表示できる文字列になるようにサニタイズしている。
残りのテンプレート文字列はただのHTMLでページがロードされると表示される。この説明では短すぎるようならtemplateパッケージのドキュメントを参照してほしい。そこには、もっと詳しい解説がしてある。
これで全部だ。有用なウェブサーバが、数行のプログラムとデータ駆動のHTMLテキストで簡単に書ける。Goはたくさんのことを数行でかけるほど強力なのだ。
訳注
サンプル中のQRは次のように引数の型を変えないと動作しなかった。ライブラリのバージョンが違うと思われる。
func QR(conn * http.Conn, req *http.Request) { templ.Execute(req.FormValue("s"), conn) }