詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Go 言語における並行処理の構築部材

5年前に買った『Java並行処理プログラミング ―その「基盤」と「最新API」を究める―』をようやく読んだ。買った頃には Perl やシンプルな JavaScript ばかり書いていたので並行プログラミングなんてほとんど気にすることがなく、実感がなくて読むのも途中で止まってしまっていた本で、家を掃除しているときに見つけたもの。その後も趣味で Android アプリを書くなど Java に触れる機会はあったけれど、せいぜいが AsyncTask を使うくらいで、マルチスレッドを強く意識してコードを書くこともなかった。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

ところがここ数年で Go を書きはじめて以来、それと気づかぬ間に並行処理をプログラムするようになっていたのがよかったらしく、今だとあれこれのトピックを身近に感じて興味深く読めた。Go の race detector が優秀で、テキトーなコードの瑕疵を指摘されているうちに感覚が身についてきたのだと思う。

Share Memory By Communicating」というスローガンでよく表わされるように、Go の並行 API はチャンネルと goroutine によるもので、かなり簡素にできている。一方 Java はsynchronized 修飾子をはじめ、Object#wait / notify とか、インタラプトの仕組みなど、フレームワークとも呼べるような仕組みが用意されていて、対照的だった。しかしそうは言っても goroutine 間で変数を共有したいことは Go を書いていても頻繁にあるわけで、参考になるところは多い。

この本では正しい並行処理を行うために、

  • データの不変項をカプセル化し、それを構成する変数を同じロックでガードすること
  • API の同期化ポリシーをドキュメントすること

を推奨している。特に後者。利用者は API の形式的な情報からそのスレッドセーフ性を判断することができないからだ。Go の標準ライブラリでも、並行処理に関わりそうなところではその旨ドキュメントされているみたいだ。

% go doc strings.Replacer
type Replacer struct {
        // Has unexported fields.
}
    Replacer replaces a list of strings with replacements. It is safe for
    concurrent use by multiple goroutines.
% go doc net/http2.Framer.WriteData
func (f *Framer) WriteData(streamID uint32, endStream bool, data []byte) error
    WriteData writes a DATA frame.

    It will perform exactly one Write to the underlying Writer. It is the
    caller's responsibility to not call other Write methods concurrently.

以下、一般的な並行処理の構築部材(ビルディングブロック)を Go でどう実現するかのパターンをメモしておく。Go 的にはこういった処理はライブラリ化するのではなくコピペもしくはイディオムとして憶えておくのが正しい態度だろうので、過度の一般化をしようとしてはいけない。

見聞きして知っていることをもとにできるだけ真摯に書くが、現実世界の並行プログラミングに詳しいわけではないので間違っていることもあるかもしれない。

ミューテックス(Mutex)

ミューテックスは複数のスレッド間で共有するリソースのアクセス権を管理する部品。これはそのまま sync.Mutex というのが存在する。チャンネルに依らない同期を行うのにたいへん重要なでプリミティブな部品だ。

sync.Mutex は zero value が未ロックを表していて、特別な初期化を必要としないので構造体に埋め込むことができる。

type syncMap struct {
    sync.Mutex
    m map[string]string
}

など。この構造体 s のフィールドを変更する前と後に s.Lock()s.Unlock() を呼ぶよう徹底できていれば、この構造体は goroutine セーフだと言える。もちろんこれらのフィールドはプライベートにしておくべきである。

構造体を作るほどではない時は、守りたい変数と並べて宣言しておく。同じ括弧の中に入れるとよりよいと思う。Mutex の Go における短い名前は「mu」である

var (
    cache map[string]string
    cacheMu sync.Mutex
)

Read-Write ロックを実現するための RWMutex というのもある。

ラッチ(Latch)

ラッチは条件が整うまでスレッドの実行を留めておくための部品。たとえば全ての HTTP GET が終わってデータが揃うまでは待機する、というようなときに使う。

これは Go においては sync.WaitGroup という名前で提供されている。これも zero value で意味を持つので、var wg sync.WaitGroup と宣言して使う。

典型的には以下のように使われる。

var wg sync.WaitGroup
for _, arg := range args {
    wg.Add(1)
    go func(arg string) {
        longRunningTask(arg)
        wg.Done()
    }(arg)
}
wg.Wait()
...

WaitGroup の Go における短い名前は「wg」である。ちなみに複数の goroutine を起動してすべての処理が完了するまで待つとき、その数があらかじめわかっている場合は、結果を通知するチャンネルを利用することもある。

c := make(chan struct{}, 4)
for i := 0; i < 4; ++i {
    go doJob(i, c) // 終わったら c <- struct{}{} される
}
for i := 0; i < 4; ++i {
    <-c
}

ブロッキングキュー(Blocking Queue)

有限または無限のキャパシティを持つキューで、キャパシティを越えて追加しようとするとき及び空の状態で取り出そうとするときにブロックする。2つのプロセスでリソースの所有権を移動しながら同期化できる。

Go おいてはこれこそがチャンネル(chan)。自明なのでコードは特になし。チャンネルの Go における短い名前は「c」である(「ch」もないわけではない)。

セマフォ(Semaphore)

(計数)セマフォは決まった数のプロセスだけが同時に並行に走ってよい、という制限を設けるためのもの。たとえば過剰な IO を避けるために利用される。

Go においてセマフォはキャパシティありのチャンネルとして実装できる。

var sem = make(chan struct{}, maxConcurrency)

func process(arg string) {
    sem <- struct{}{}
    ...
    <-sem
}

func run() {
    for _, arg := range args {
        go process(arg)
    }
}

これ初めて見たときはよく分かんなかった。セマフォの Go における短い名前は「sem」である

Future

Future (カタカナで書かない気がする)は後々必要になる値の計算や IO をあらかじめスタートさせておきたいような場合に使うパターン。結果を取得するときにブロックさせて待つこともできる。自分は実際に書いたり読んだりした経験はないのだけど、Future パターンを意識して書くならこんな風に書けると思う。

struct futureInt {
    result int
    done chan struct{}
}

func (fut *futureInt) Get() int {
    <-fut.done
    return fut.result
}

func calcInt() futureInt {
    fut := futureInt{
        done: make(chan struct{}),
    }
    go func() {
        fut.result = longRunningTask()
        close(fut.done)
    }
    return fut
}

バリヤ(Barrier)

こちらは参考までに記しておく。Java の CyclicBarrier というクラスは再利用のできるラッチのようなもので、スレッド群を繰り返し同期化するような場合に便利らしい。

検索すると sync.Cond を使ったあまり簡単ではない例も見つかるのだが、golang-dev には goroutine は十分軽量だから毎回新しいのを起動したらいいよ といった話もあり、そっちのほうが Go らしいようにも思った。

まとめ

プログラム言語の話に限らず、ある領域の見聞や経験を深めたあとでは他の領域から得られる知識の吸収の仕方の質が異なるもので、思ったより参考になる話が多かった。とはいえ Go には Go のやり方があるものなので、あまり Java におけるクラスやパターンをそのまま流用しようとするのはよくない傾向であることも意識しておかなければならない。Go を読み書きする際には(パターンというほどでもない)イディオムを知っておくことに意味がある言語なので、こうやってまとめておくことも無駄ではないだろうと思う。

参考文献

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

AsciiDoc(Asciidoctor)の文書をtextlintで校正する

AsciiDocはMarkdownのようなテキストマークアップ言語のひとつで、ページ内リンクや脚注などかなり機能が豊富なので、大きめのドキュメントを本腰を入れて書くなど、表現したいものがある程度複雑なときに便利。DocBookというフォーマットを通じて、HTMLだけでなくPDFとかroff形式にも変換できるらしい(やったことはない)。GitのドキュメントがAsciiDoc形式で書かれているのを見て知って、自分も個人的なドキュメントはREADME.adocなどとして、AsciiDoc形式で書くことがある。AsciidoctorはAsciiDocの実装のひとつで、Rubyで書かれていて利用が簡単なのでこれを使っている。

さて、上記の記事でtextlintなるものを知ったので、AsciidoctorのJavaScript実装を使ってプラグインを書こうと思いたったわけだ(標準で対応しているのはプレーンテキスト、HTML、Markdownのみ)。しかし取りかかってみるとこれがなかなか大変で、textlintの期待するデータ構造はテキストの行やカラム位置の情報まで求めるので、Asciidoctorのパーザに精通している必要がありそうだった。せいぜい校正項目のある行くらいが分かれば自分の目的は達成されるので、ここまでやるのは少し苦労が多そうだった。

そこで、一度プレーンテキストに変換してからtextlintで処理し、結果のエラーの行番号を元のファイルの行にマッピングする、という方法を取ることにしたのがこちら。npm install textlint してある前提。

Rubyのスクリプトで、指定されたファイルをAsciidoctorのAPIで解析し、構文木をたどりながらプレーンテキストを吐く。その際に各行と、そのソースの位置情報を記録しておく。プレーンテキストに対してtextlintを走らせて、結果をJSONで受けとり、元のファイル名に直して出力する。そのままVimのquickfixリストにできるフォーマット。

少しだけ変なハックを入れている。このスクリプトは通常はHTMLタグのみを消して出力する、つまりアンカーテキストなどは校正の対象となるのだけど、それで上手くいかない場合があった。codeタグの中に入ったアルファベットも日本語のルールで校正されてしまう、という問題だ。そこで -T オプションでそのタグの内容を墨塗りできるようにしてある。中身を消し去ってしまうと変な文章になるので、苦肉の策。墨塗り文字のくり返しがエラーになるので、そいつは無視する、ということもしている。

第三者のための『リーダブルコード』

新人エンジニアにオススメの本のひとつだよねー、などと言いつつ自身は読んだことがなかったので慌てて買って読んだ。

お題「リーダブルコード」

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

  • 作者: Dustin Boswell,Trevor Foucher,須藤功平,角征典
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2012/06/23
  • メディア: 単行本(ソフトカバー)
  • 購入: 68人 クリック: 1,802回
  • この商品を含むブログ (131件) を見る

なるほど当たり前のようなことでありながら短期的に意識して身につけるのは難しかったことばかりで、これがこのようにまとまった形の書籍になっていることはとてもありがたいことだと思う。規約を押しつけてるわけじゃなくて指針の集合なので、気楽さもある。

プログラム中で使用する英語動詞

本の中には「名前に情報を込める」という話があった。

非英語話者でありながらプログラミングに英語を使っている身としては、今ひとつニュアンスが分からない語がある一方、却って巷のソースコードから英語の利用例を知る割合が高くなるわけで、そうやって英語のプログラミング向けのサブセットを作ることもできそうだなと思う。

そこで現在こんな動詞をこういうニュアンスで使っていますよ、というのを雑に列挙してみる。語彙は使用するプログラミング言語やその著名なライブラリの語彙にかなり影響を受けるので、普遍的なものにはなり得ない。

英語 用途 備考
search データベースなどからの複数のデータを取得する。 なんで select じゃないのかは不思議
find データベースなどから条件にしたがってデータをひとつ取得する。
insert データベースへデータを挿入する。 SQLからの連想
delete データベースからデータを削除する。 SQLからの連想。データ構造からのデータの削除にも使う
update データベースのデータを更新する。 SQLからの連想
load 外部リソースをメモリ上のモデルへ展開する。 load したものは save する
new (これは動詞ではないが)インスタンスを生成する。 言語の機能になってることが多い
make データ構造を作成する。
generate IOをともなわずに値を生成する。
fetch HTTPなど比較的重いIOによってデータを取得する。
request HTTPリクエストを送信する。
require データ構造のある部分を要求する。存在しなかったら例外を投げる。
ensure データ構造のある部分が存在しなければ作成する。

addとかremoveとかもっといろいろあるやん……と思ったけどキリがないのでやめておく。

これらは低レイヤの処理として使われたときにこういう名前を使いますという類のもので、より抽象度の高いレイヤにおいてはもっと自由に名づけられると思う。こうやって読んでみるとどんなリソースにアクセスするのか、を意識してるな。

ちなみに自分は ensure を上記のように考えていたんだけど、require のように使う、という意見もあった。たしかにプログラムの次の行に到達したときに、求める状況になっていることを確実にできるという点では同じであるなあと思う。

コメント中の意見に署名する

TODOなんて書かれたコメントは放置されるものだ。というか、放置されたものだけが生き残るわけなので、他人に見られるのは放置されたものばかりになるのだといえる。そして生まれてすぐの早いうちに誰も対処しなかったTODOは、やがて消して良いのかどうかの判断もできなくなってコード中の定位置を確保する。

そこで最近はTODOやらFIXMEといったコメントを書くとき、# TODO(motemen): 処理を切り出す というように、自分の名前を入れるようになった。これはGoを書いているうちに知ったものだけど、まあ源流がどこかにあるんだろう。この署名を加えることで、例えば手を付けられないままそのTODOが生き残り誰かの目に触れたとしても、ははあこれはあいつに訊けばよいのだなとなって、まったく判断できないということは避けられる。その他同様に、コメントにコードを補足する事実でなく意見を書くときには、誰が書いたかを明らかにする。

プロジェクトによっては人名だけでは足りないとして、詳しい情報をつけ加えるようにしているところもある模様。

ソースコードの第三者

……というようなことを考えながら読んだ。その他にもコードの役割を細かく、とか制御構造を読みやすく、という話題があったけど、それらはコードに向き合っているうちに自ずと実現されるように思う。

10年前の自分は知らなかったことだけど、プログラミングにおいてコードを書く際には、書き手の自分とそれを受けとるコンピュータのほかに、第三者としてソースコードの読み手が存在する。昔の自分も当然その存在を知ってはいたものの、ちゃんと認識できてはいなかった。プログラマは読み手として多くの時間を過ごすものだと気づけたのは、実際にそれだけの時間を費やしてからだった。いや、それよりも自分が過去に書いたコードを他人に読まれるようになったからかな……。

人間が書いたプログラムの意味(何をするべきか)をコンピュータは解釈して間違いなく書かれたとおりに実行するけれど、ソースコードがそのまま人間の意図(何をしたいか)や書かれた文脈(何故そうするのか)を表現しているとは限らない。最悪、意図と意味が異なっていることだってある。第三者がコードを読むとき、コンピュータのために書かれた意味から意図を取りだすことは簡単だとは限らない。書き手のほうはもちろん意図を実現するものとしてコードを書いているから、この差に気がつかないと、後から読んだときに読み手が困るようなコードが出来上がることになりがち。

もちろん、デザインパターンとか、名前のついたアルゴリズム、フレームワークなど人間のあいだの共通言語を強化することでソースコードの意図を分かりやすくすることはできる。また、抽象度を高めることで、書かれたコードと意図とのあいだの距離を縮めようとするって手もある。その場合も抽象化を受け持つ層のことが人びとによく共有されていないと結局動きを把握するためにコードを潜るはめになって、却って苦労が増すことになる。このあたりは業界の潮流とかプログラミングのパラダイムにも大きく影響を受けそうなところ。


なんか、若い人のほうがこういうところきっちりできる印象なので(冒頭に書いた言葉とはうらはらに)、むしろ自分の身につまされる本だった。プログラムって人間とコンピュータのあいだの言葉であるのと同じくらい、人間と人間のあいだの言葉でもあるなと思った。

この記事は次の酒を飲んでいる最中に書かれました:

はてなで一緒に働きませんか?