要約
注意: 2018年11月5日にconcurrent-ruby 1.1がリリースされ、Promises APIはデフォルトで含まれるようになりました。
- concurrent-rubyの1.0.5以前のバージョンにはPromises APIは含まれていない
- Promises APIを使いたいときは、concurrent-ruby-edge gemを追加する
動機
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です。
色々な言語から並行プログラミングの仕組みを取り込んで来ました。
- Clojureから FutureとDelayを
- JavaScriptから Promiseを
- I-structures: data structures for parallel computingから IVarを
取り込んで来ました。
他のプログラミング言語から来た人の期待に答えられる一方で、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が使えます!
concurrent-rubyのv1.1.0がリリースされた暁には、concurrent-rubyのバージョンをあげ、concurrent-ruby-edgeを消せば、対応できるでしょう。
-
https://github.com/rails/rails/blob/b2eb1d1c55a59fee1e6c4cba7030d8ceb524267c/activejob/lib/active_job/queue_adapters/async_adapter.rb ↩
-
https://github.com/rails/rails/blob/b2eb1d1c55a59fee1e6c4cba7030d8ceb524267c/actioncable/lib/action_cable/server/worker.rb ↩
-
https://github.com/rails/rails/blob/b2eb1d1c55a59fee1e6c4cba7030d8ceb524267c/activerecord/lib/active_record/type/type_map.rb ↩