Emacs の master ブランチに来たスレッド機能についての雑なまとめ

メモ的な感じ。

実装の概要

  • make-thread によってスレッド開始
  • thread-yield で別スレッドにスイッチ
  • thread-join で待ち合わせ

  • make-mutex で mutex 作成

  • with-mutex とかで排他制御
  • make-condition-variable で condvar 作成
  • condition-wait, condition-notify でスレッドの制御

  • グローバルの状態を共有

  • スレッドごとに local, dynamic variable, current buffer, match data は独立

  • GILがあって、同時にひとつしか動かない。

  • 基本はユーザーが thread-yield する協調スレッド動作で、thread-yieldかIOかイベント待ちでスレッド切り替え
  • ただし、fine-grained な制御を想定して設計しているので、現在の協調スレッドの動作に依存しないように注意すべしとのこと。

動作確認したいけど、ちょっと動かすとすぐ落ちるのでなかなか難しい。(追記 2016/12/18 の先端では落ちなくなった。まだいろいろ調整中の模様。)

後方互換性の問題

仕様や機能を追加した際に、既存のコードが壊れない方がうれしい。 特に、Emacsは長年の資産が重要なので、互換性を壊すコードにはそれなりの動機が必要なはず。 実際に、これまでも滅多なことでは過去の動作を変えていないし、大抵長期間の移行措置がある。cl-libとかいろいろ。

make-thread は既存のコードをカジュアルに並行化出来てしまう。 以下のマクロは実際にMLに投稿されたもので、既存の関数を別スレッドで動かそうというもの。gnusというニュースリーダーで使ったところ、UIが固まらなくて便利だったとのこと。 *1

この例のマクロでは、一見排他制御があるように見えるが、もともとの関数を呼ばれると2重に起動されてしまう。 グローバルの状態を共有するため、同時に動作しうる箇所にはもれなく排他制御などが必要。

何も触っていない既存のコードについても、IOがからむ箇所は同様に重複して呼ばれる可能性がある。 特に、上の例のようにIOで遅い箇所は積極的にスレッドで呼ばれる可能性があるため、呼ばれそうなところをもれなくチェックして、必要なら排他制御を入れる。 つまり、スレッドを使うと既存のコードが何もしなくても壊れる。

排他制御がないと、大体何となく動くけど、たまに落ちる、動いてるけどバッファとかが見えてないところで壊れるなどが起きる。 中途半端に動いてデータがこっそり壊れてたり、再現がなかなか出来ないバグがとにかくつらい。

問題を解決しない

現在の実装では同時に1スレッドしか動かないので、CPUが原因で固まる問題は解決できない。 複雑な構文解析や巨大なファイルのシンタクスハイライトで文字入力の度に固まるような問題は解決できない。

IOが原因でUIが固まる問題については解消される場合がある。上の例ではgnusがそうだった。

しかしながらスレッドが無い現状でも、IOで固まる問題は既存の非同期IOのインタフェースを使えば解決できる*2。 orgやgnusぐらいでかいと非同期プログラミングで書きなおすのは大変だけど、どうせ後方互換性が壊れるのであれば、既存のコードを書き直す方が良いかもしれない。フリーランチは無い。 *3

そもそも非同期プログラミングは、コールバックとかのプログラムの書き方が難しいのではない。 並行処理をまともに動かすことが難しいのであって、同期的に書けることはあまり解決になってない。 シングルスレッドな設計のまま安易に排他制御を入れると、ロックだらけになってロック漏れやデッドロックでつらいはず。 *4

あと、個人的には協調スレッド動作を止めれてもGILが外せるとは思えないので、完全なマルチスレッドは来ないのではないかと考えている。 *5

その他の問題

mutex でロックするにしても、知らない mutex ではロックできない。 複数の mutex が関わると容易にデッドロックする。mutex 一つでも間違えるとデッドロックする。 ロックを使うパッケージは自由に合成できない。別々なら正しくても、混ぜると壊れることがある。

他のスレッドAPIのある言語がそうであるように、各関数のドキュメントにスレッドセーフかどうか明記する必要がある。

このAPIの上に行儀の良いラッパーをかぶせても解決できない。 グローバル共有なので全員がラッパーを使って、ルールを守ったプログラムにする必要があるが、恐らく不可能。 ドキュメントは読まれないし、ルールは守られない。

他の言語(JavaとかRubyとかPythonとか)ではプリミティブなスレッドAPIでも上手くやってるように見えるかもしれない。 これらの言語では、基本的にプロジェクトごとに並行系ライブラリを選ぶことが出来たり、マルチスレッドプログラミングの方針を統一できるので、ましな方法で統一させることが出来る。そもそも異なる制御方針のライブラリは無理やり混ぜても大体動かない。 Emacsでは、どのパッケージをまぜて使いたいかというのは開発者ではなくユーザーが選べるべきであるので、マルチスレッドの管理方針が違うくらいでパッケージが混ぜられないというのは避けたほうが良い。 だから、なるべくお互いに影響を与えないようなしくみが望ましい。

どうすればよかったか?

後方互換性が壊れず、問題も解決する(ちゃんと同時に複数実行が出来る)手法があると考えている。

ブラウザの WebWorker や、golang の goroutine/channel などがお手本になりそう。 グローバルやスレッドセーフでないオブジェクトを共有せず、完全に並行で動かして、通信(イベントやチャネル)でデータを共有する。 パフォーマンスの問題や、バッファの共有をどうするか、ライブラリはどうするかなどいろいろ考える必要はあるが、WebWorker の Transferable Object の仕組みのように、個別に対応して落とし所を探っていく進め方になると思う。 安全な仕組みをちゃんと作るのは大変そうだし、現在のEmacsの実装で可能かどうかはよく分からない。検討したけどやめたのかも知れない。 *6

この方法で上手く設計できればスレッド間の排他制御のためのロックが必要なくなるので、ロックなどの問題からも解消される。 メモリは独立なので後方互換性も壊れないし、独立して動くのでCPUで詰まる問題も解消出来る見込みが出来る。

時代遅れと指摘されているのは、すでにこのような仕組みが他の言語で取り入れられて実装されているにもかかわらず、問題が多いとわかっているメモリ共有で thread と mutex による制御を取り入れたこと。 *7

アクターモデルやイミュータブルデータモデルなど、マルチコアの時代になって、ロックを使わない並行プログラミングが広まっている。 ただし、シングルスレッドを前提にした単純なプログラミングモデルのまま移行は出来ないので、並行プログラミングの設計を身につける必要がある。

もちろん、プリミティブなthreadやロックによるプログラミングを全て否定するわけではない。 マルチコアを使い切るようなアプリケーションや、処理系の実装などの低レイヤーのプログラミングなど、それが必要とされる分野もある。 *8

大騒ぎするほどでもないのではないか

ちゃんと議論を全部追ったり、コードを全部見たわけではないので、自分が盛大に勘違いしてることがあるかも知れない。

また、現実的にはあまり make-thread するようなコードを書く人なんていなくて、全く問題が発生しない(報告されない)かも知れない。 実際、MLで「いやいや並行制御は大変なんですよ」って書いたら、Emacsでそんなガチ制御なんて必要ないと思うよと返ってきた。

あるいは、メインメンテナのみなさんによるスーパーテクノロジーで、今後なにかすごい進化が起きるのかも知れない。

とにかく、今は自分が本体に十分コミットできる状況ではないので、この流れを見守りたい。 *9

*1:そうとうやばいコードのはずなのに並行性について指摘がないことにびっくりしてたけど、あとで同様にびっくりした人から並行性についてのコメントが付いててちょっと安心した。

*2:同期的コードを非同期インタフェースで書き直す話 非同期と継続と私 - 技術日記@kiwanami

*3:フリーランチについて、例えば 後藤弘茂のWeekly海外ニュース など

*4:deferred.el について補足いろいろ - kiwanami の

*5:そもそも協調スレッドをやめるのなら、IO以外でも割り込まれるわけなので、そこら中に排他制御を入れる必要がある。つらそう。

*6:2014年の段階ではブランチを作った本人は非共有状態でメッセージパッシングがいいという議論をしていたという話がある。concurrency and emacs - a surprising next step

*7:A Blast From The Past: The Tale Of Concurrency In Emacs - Sebastian Wiesner

*8:O'Reilly Japan - 並行コンピューティング技法

*9:でも、こういう問題を見ると、それ cc:semaphore で…とかすでにcacoo.el で通ったしとか、いちいち思ったりする。