Edited at

concurrent-ruby1.0.5以前でのPromisesの使い方


要約

注意: 2018年11月5日にconcurrent-ruby 1.1がリリースされ、Promises APIはデフォルトで含まれるようになりました。


動機


Ruby on Railsアプリケーションのスレッド処理にconcurrent-rubyを使いたい

Ruby on Railsアプリケーション中でスレッドを使ったプログラミングをしています。

Ruby on Rails 自体ではスレッドを扱う時に、Ruby標準ライブラリのThreadをそのまま使うのでなく、concurrent-rubyを使っています。123

アプリケーションプログラム中でスレッドを扱う際も、同じライブラリを使った方が機能の重複が減り、保守性が高まるでしょう。


concurrent-rubyの具体例

「非同期に処理を並行実行し、すべての処理の完了を待つ」処理をThreadで書くと次のようになります。

threads = [1, 2, 3].map { |val| Thread.start { pp val } }

threads.each(&:join)

これをconcurrent-rubyを使うと、次のように書けます。

tasks = [1, 2, 3].map { |val| Concurrent::Promises.future { pp val } }

Concurrent::Promises.zip(*tasks).value!

少し長くなります。ソースコードとしてはあまり嬉しくありません。

concurrent-rubyは、スレッドを管理するスレッドプールを保持しています。スレッドプールの上限サイズを指定することで、同時に実行されるスレッド数を制限できます。想定外の入力値によって大量のスレッドが作られ、メモリを使い切る事故を防止できます。スレッド生成数を制限する処理を自分で書くと面倒です。ありがたいです。


concurrent-rubyの状況


リリース予定の(複数形の)Promises

concurrent-rubyは、Rubyの並行プログラミングをサポートするgemです。

色々な言語から並行プログラミングの仕組みを取り込んで来ました。

取り込んで来ました。

他のプログラミング言語から来た人の期待に答えられる一方で、Rubyプログラマにはどういうスタイルで並行プログラミングを書けばよいかを示してきませんでした。

今、乱立した作法を統一したPromisesという名前の新しいAPIを組み直しています。masterブランチのREADME、つまりGithubのトップページは、新しいPromisesを対象として説明しています。Deprecatedセクションには


Promise: Similar to Futures, with more features. Replaced by Promises.


と書かれています。Promisesを使うべきと推奨しているように読めます。


Promisesの例

例えば、非同期に処理を並行実行し、すべての処理の完了を待つ処理を(以前の)Promiseで書くと次のようになります。

promises = [1, 2, 3].map { |val| Concurrent::Promise.execute { pp val } }

Concurrent::Promise.zip(*promises).then{|val| valを処理する }

これは、Promises版の

tasks = [1, 2, 3].map { |val| Concurrent::Promises.future { pp val } }

Concurrent::Promises.zip(*tasks).value!

と似ていますが、戻り値が違います。

Promise版はPromiseが帰ってきます。処理が完了したときの処理は、thenブロックの中でvalを処理する必要があります。同期的なRubyのコードにPromiseが帰ってきても、同期処理には戻れません。同期処理の一部を非同期処理に置き換える際に、非常に悩まされる部分です。

(新しい)Promisesにはvalue/value!のブロッキングメソッドが追加され、これらのメソッドを呼ぶだけで、同期コンテキストに制御フローを戻せます。同期プログラミングが圧倒的に多いRubyに、必要な非同期処理はこれです!


問題


1.0.5以前のバージョンにはPromisesは含まれない

Promisesは、1.1からリリースされました。

v1.0.5にはPromisesは含まれていません。

v1.0.5を使っている環境でConcurrent::Promisesを呼ぼうとすると

uninitialized constant Concurrent::Promises

定数が存在しない旨のエラーが出ます。

require 'concurrent/promises'

を実行して、モジュールを読み込もうとすると

No such file to load -- concurrent/promises.rb (LoadError)

ファイルがない旨のエラーが出ます。

concurrent-ruby gemでインストールされているフォルダ/usr/local/bundle/gems/concurrent-ruby-1.0.5/lib/concurrent/を確認すると、promises.rbというファイルはありません。(注意:gemがインストールされるフォルダは環境によって異なります。bundle config pathコマンドで確認してください。)


回避策

Promises APIは突然生まれてきたものではありません。

v1.1.0.pre1のリリースノートによると


Promises are moved from concurrent-ruby-edge to concurrent-ruby


moved from concurrent-ruby-edgeとあります。

concurrent-ruby-edge gemにpromises.rbが含まれています。

Gemifleにconcurrent-ruby-edgeを追加、bundleコマンドを実行し、ソースコードに

require 'concurrent/edge/promises'

を追加、Concurrent::Promisesクラスを読み込めば(複数形の)Promisesが使えます!:tada:

concurrent-rubyのv1.1.0がリリースされた暁には、concurrent-rubyのバージョンをあげ、concurrent-ruby-edgeを消せば、対応できるでしょう。