通信によってメモリを共有する
Share Memory By Communicating by Andrew Gerrand
伝統的なスレッドモデル(たとえばJava、C++、Pythonでプログラムを書く際によく使われます)では、 プログラマはスレッド間の通信に共有メモリを使う必要があります。通常、共有されるデータ構造は ロックによって保護され、スレッドはそのロックを奪い合いながらデータにアクセスします。 場合によっては、PythonのQueueのようなスレッドセーフなデータ構造を使うことで、 これが多少楽になることもあります。
Goの並行処理の基本要素であるゴルーチンとチャネルは、並行処理を行うソフトウェアを構成するための、 エレガントでかつ他とは一線を画す手段を提供します。(これらの概念には 興味深い歴史があり、C. A. R. Hoareの Communicating Sequential Processesに端を発しています。) 共有データへのアクセスを調停するために明示的にロックを使うのではなく、Goはゴルーチン間で データへの参照をチャネルでやり取りすることを推奨しています。このアプローチによって、 ある時点でそのデータにアクセスできるゴルーチンは常に1つだけであることが保証されます。 この考え方は、Effective Go(Goプログラマなら 誰もが読むべき文書です)の中で次のようにまとめられています。
メモリを共有することで通信するのではなく、通信することでメモリを共有せよ。
URLのリストをポーリングするプログラムを考えてみましょう。伝統的なスレッド環境では、 データを次のように構成することになるかもしれません。
type Resource struct {
url string
polling bool
lastPolled int64
}
type Resources struct {
data []*Resource
lock *sync.Mutex
}
そして、Pollerという関数(これは多数が別々のスレッドで動作することになるでしょう)は 次のようなものになるかもしれません。
func Poller(res *Resources) {
for {
// 最後にポーリングしてから最も時間が経っているResourceを取得し
// ポーリング中であるとマークする
res.lock.Lock()
var r *Resource
for _, v := range res.data {
if v.polling {
continue
}
if r == nil || v.lastPolled < r.lastPolled {
r = v
}
}
if r != nil {
r.polling = true
}
res.lock.Unlock()
if r == nil {
continue
}
// URLをポーリングする
// Resourceのpollingとlastpolledの各フィールドを更新する
res.lock.Lock()
r.polling = false
r.lastPolled = time.Nanoseconds()
res.lock.Unlock()
}
}
この関数はおよそ1ページ分の長さがあり、完全なものにするにはさらに詳細が必要です。 URLをポーリングするロジック自体(それ自体はほんの数行で済むはずですが)すら 含まれていませんし、Resourceのプールを使い果たした場合をうまく処理することもできていません。
同じ機能をGoのイディオムを使って実装したものを見てみましょう。この例では、Pollerは 入力チャネルからポーリングすべきResourceを受け取り、処理が終わったらそれを出力チャネルに 送信する関数です。
type Resource string
func Poller(in, out chan *Resource) {
for r := range in {
// URLをポーリングする
// 処理済みのResourceをoutに送信する
out <- r
}
}
先ほどの例にあった繊細なロジックは見事になくなり、Resourceのデータ構造にはもはや 管理用のデータも含まれていません。実際、残っているのは重要な部分だけです。これによって、 こういったシンプルな言語機能が持つ力の片鱗を感じ取れるはずです。
上記のコードスニペットには多くの省略があります。この考え方を使った完全でイディオマティックな Goプログラムを一通り見ていくには、コードウォーク Share Memory By Communicatingを参照してください。
By Andrew Gerrand