@ledsun blog

無味の味は佳境に入らざればすなわち知れず

一度moveしたオブジェクトは移動先のRactorが死んでも帰ってこない

moveしたオブジェクトを参照するとRactor::MovedError

次のRubyスクリプトがあります。

dead = Ractor.new {}
dead.send STDIN, move: true
STDIN.gets

RactorにSTDINをmoveしてから、STDINを使います。 するとRactor::MovedErrorが起きます。

~ ruby dead_and_move.rb
Traceback (most recent call last):
        1: from dead_and_move.rb:5:in `<main>'
dead_and_move.rb:5:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)

move先Ractorが死んでもRactor::MovedError

次のようにmove先のRactorが終了するのを待ちます。

dead = Ractor.new {}
dead.send STDIN, move: true
dead.take # Ractorの終了を待つ
STDIN.gets

結果は変わらず、Ractor::MovedErrorです。

~ ruby dead_and_move.rb
Traceback (most recent call last):
        1: from dead_and_move.rb:4:in `<main>'
dead_and_move.rb:4:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)

moveしたオブジェクトを再び使うには再びmoveする

次のように、Ractorで受け取ったオブジェクトを再度moveすると、メインスレッドで使えます。

dead = Ractor.new do
  Ractor.yield Ractor.recv, move: true # 受け取ったオブジェクトを送り返す
end

dead.send STDIN, move: true
dead.take.gets

一度moveしたオブジェクトは自動的には帰ってきません。 再び使いたければ、明示的に戻します。

国家公務員法改正案に関するメモ

国家公務員法改正案が廃案になるそうなので、どういう話だったのかメモを集めておきます。

法案

衆法 第196回国会 30 国家公務員法等の一部を改正する法律案

議案というのがパッチで、要項がdescriptionみたいです。

第201回 通常国会|内閣官房ホームページ

こっちのほうが解説PDFと現時点の法案が載っていてわかりやすいです。 法案そのものは、現時点の僕の知識で読み解くのは難しいです。

平均寿命の伸長や少子高齢化の進展を踏まえ、知識、技術、経験等が豊富な高 齢期の職員を最大限に活用するため、定年の65歳引上げについての国会及び内閣 に対する人事院の「意見の申出」(平成30年8月)に鑑み、国家公務員の定年を 引き上げる。

意見

橋下徹氏が“束ね法案”国家公務員法改正案の問題点を指摘 「60歳以上の給与7割保障は疑問です」:芸能・社会:中日スポーツ(CHUNICHI Web)

国家公務員法の改正によって60歳以上の給与7割保障は疑問です。それをやるなら民間にも7割保障を義務化すべきだと思います」と言及。さらに「民間に義務化ができないなら、公務員もそのときの民間水準に合わせるという仕組みが筋なのではないでしょうか?」

https://twitter.com/hashimoto_lo/status/1262546929986138112

連合|国家公務員法等の一部を改正する法律案において措置されてい...(事務局長談話)

連合は、公務員の65歳への定年の引き上げは諸環境の変化等を踏まえた、不可欠な勤務条件であり、必要とされる社会的政策と認識してきた。従って、本改正案については、「検察官の勤務延長規定を削除すれば賛同できる」と表明している野党(共同会派)が主張しているとおり、本来であれば国家公務員法改正法案から切り離した上で、検察官人事における内閣の関与の範囲など丁寧な検討が必要である。その上で、国民の疑念に対し真摯に対応すべきである。

高橋洋一の霞ヶ関ウォッチ 「定年延長」国家公務員法改正案は、黒川氏人事とは関係ない: J-CAST ニュース【全文表示】

国家公務員定年延長には長い経緯がある。2008年国家公務員制度改革基本法中に65歳まで定年延長は盛り込まれている。その法律は福田康夫政権のときだが、その企画立案の一人として筆者も関わり、当時の民主党の協力で成立した。その後2回(2011年9月、2018年8月)の人事院から政府への意見申出、3回(2013年3月、2017年6月、2018年2月)の閣議決定を経て現在にいたる。

新展開…自民・世耕氏が検察官含む公務員の定年延長自体に異論 新型コロナ受け…検察庁法改正案の行方は?

この景気状況、雇用状況の中で本当に(公務員の)定年延長していいのかどうかも含めて、立ち止まってしっかり議論することが重要だ

いったい検察庁法改正案の何に抗議しているのか|徐東輝(とんふぃ)|note

個人的には、人生100年時代において、民間と同じく国家公務員も定年を延長することに異論はありません。さらに、人件費削減のために役職定年制を設けることにも賛成ですし、もっというと、特殊なケースで役職定年制に特例を設けることにも賛成です。  しかし、どうもまだこの「特殊なケース」の判断軸が見えてこない。

検察庁法・国家公務員法改定案/塩川氏の質問 衆院本会議 (日本共産党塩川鉄也議員)

国家公務員法等改正案の最大の問題は、憲法の基本原理である権力分立を破壊する検察庁法改正案を入れ込んでいることです。

東京高検黒川弘務検事長の定年延長を行った閣議決定の撤回を改めて求めるとともに、国家公務員法等の一部を改正する法律案のうち検察庁法改正案に反対する会長声明|決議・声明|東北弁護士会連合会

この改正案によれば、内閣及び法務大臣の裁量によって検察官の人事に介入をすることが可能となり、検察に対する国民の信頼を失い、さらには、準司法官として職務と責任の特殊性を有する検察官の政治的中立性や独立性が脅かされる危険があまりにも大きく、憲法の基本原理である権力分立に反する。

徳島弁護士会 検察庁法に違反する定年延長の閣議決定の撤回を求め、国家公務員法等の 一部を改正する法律案に反対する会長声明

特例措置が受けられるという法律に改正されてしまうと、時の政府の意向次第で、検察庁法の規定に基づいて上記の東京高検検事長の定年延長のような人事が可能になってしまう。これは、汚職事件など政界が絡む犯罪に対し、強い職務権限を持ち、準司法的役割を担う検察官の政治からの独立性・中立性の確保という検察庁法の趣旨を根底から揺るがすものであり、到底、容認することはできない。

国家公務員の定年を段階的に65歳に引き上げ 役職定年制も導入(国家公務員法等の改正法案を国会に提出) | 社会保険労務士PSRネットワーク

予定どおりに国家公務員の定年が65歳となれば、民間企業においても、定年自体の引き上げが本格的に検討されることになりそうですね。 なお、民間企業については、今のところ、定年の引き上げの予定はありませんが、高年齢者雇用安定法の改正案(現国会で審議中)により、令和3年4月から70歳までの就業機会の確保が努力義務とされる予定です。

検察庁法改正案はクロかシロか | いさ進一 活動の記録 (公明党のいさ進一議員)

公務員の共済年金と厚生年金が一元化され、公務員の年金の支給開始年齢も、会社員と同様に徐々に引き上げられています。最終的には支給開始は「65歳」からとなりますが、その途上にある現在では、支給開始年齢は「64歳」です。今回の法案が成立して施行され、検察官の定年も「65歳」となれば問題ありません。しかし、現在の検察官の定年は「63歳」です。つまり、64歳までの一年間、仕事もなく年金もないという状況が発生することとなります。しかも、一般の国家公務員では認められている「勤務延長制度」も「適用されない」こととなっています。今回、定年を「65歳」に合わせるのであれば、「勤務延長制度」も他の国家公務員同様、適用させることにしたらよいではないか、法務省はそう考えました。

検察庁法の一部改正法案の撤回及び違法な定年延長の閣議決定の撤回を求める会長声明 | 長崎県弁護士会 - NAGASAKI BAR ASSOCIATION

「全ての検察官の定年を一律に65歳まで延長すること」や「63歳の役職定年を設定すること」自体について反対するものではないが、「内閣又は法務大臣の裁量による役職延長や勤務延長」を認める部分については強く反対する。

日キ靖国神社問題特別委員会 「検察庁法改正案」に反対声明 2020年5月12日 | キリスト新聞社ホームページ

現在、国会に提出されている当該改正案は、政府や行政を監視すべき検察庁の機能を骨抜きにし、三権分立と民主主義とを崩壊させる危険な改悪であり、わたしたちはこれに強く抗議するとともに速やかな撤回を要求します。

検察庁法改正案の中身がやっと理解できたよ | ジャーナリスト神保哲生 official blog

内閣に気に入られれば2年から最長で5年もの長きにわたり今のポストにとどまれるのはもちろんのこと、場合によってはもう一つ上のポストも狙える一方で、どんなに優秀な検察官でも内閣に煙たがれれば63歳でお払い箱ということになり、内閣が検察幹部の人事に対する絶大な裁量を手にすることになります。

前川喜平(右傾化を深く憂慮する一市民) on Twitter: "要するに、2022年4月の改正法の施行までは現行国家公務員法により、施行後は改正検察庁法により、黒川氏の定年は自在に延長できるわけだ。2025年2月の68歳誕生日まで黒川氏を検事総長に据え置くこともできる。検察庁法再改正で定年を70歳にすれば2027年まで据え置ける。 #検察庁法改正案に抗議します"

2025年2月の68歳誕生日まで黒川氏を検事総長に据え置くこともできる。検察庁法再改正で定年を70歳にすれば2027年まで据え置ける。

経緯

2018年 「定年を段階的に65歳に引き上げるための国家公務員法等の改正についての意見の申出」人事院

人事院の意見の申出 より https://www.jinji.go.jp/iken/30mousidekossi.pdf

ここから今回の法案が出てきて、廃案になったようです。 意見の申出は次の閣議決定に基づいているそうです。

2017年 「経済財政運営と改革の基本方針2017」閣議決定

経済財政運営と改革の基本方針2017 - 内閣府 より https://www5.cao.go.jp/keizai-shimon/kaigi/cabinet/2017/2017_basicpolicies_ja.pdf

⑦ 若者が活躍しやすい環境整備、高齢者の就業促進 就職氷河期世代や若者の活躍に向けて、職務経歴、職業能力等に応じた集中的な正社 員化支援等を行う。また、高校中退者等の高卒資格取得に向けた学習相談・支援を行う。 65 歳以降の定年延長、継続雇用延長等を行う企業への支援を充実し、継続雇用年齢等 の引上げを進めていくための環境整備を行う。2020 年度(平成 32 年度)までを集中取 組期間と位置付け、助成措置の強化等を行い、集中取組期間の終了時点で、継続雇用年 齢等の引上げに係る制度の在り方を再検討する。公務員の定年の引上げについて、具体 的な検討を進める。また、多様な技術・経験を有するシニア層が、幅広く社会に貢献で きるよう、ハローワークにおける求人開拓を強化する。

2020年までに高齢者の就業促進をすることを決めていて、公務員の定年延長も含まれていたようです。

2013年 「国家公務員の雇用と年金の接続に関する基本方針」国家公務員制度改革推進本部行政改革実行本部決定

今後の公務員制度改革の在り方に関する意見交換会(第3回)議事次第 よりhttps://www.gyoukaku.go.jp/koumuin/arikata-ikenkoukan/dai3/sankou3.pdf

退職共済年金の支給開始年齢の引上げに伴い、再任用により雇用と年金の接続を図る 方針を決定

2013年から年金支給年齢の引き上げが始まるので、再任用で年金支給までの期間を埋めました。 このときは定年延長は案としてあっても、確定しなかったようです。

2011年「定年を段階的に65歳に引き上げるための国家公務員法等の改正についての意見の申出」人事院

人事院の意見の申出 より https://www.jinji.go.jp/iken/23mousikossi.pdf

2013年から年金支給年齢の引き上げが始まるので、2013年から2025年までの期間で、国家公務員の定年を65歳まで引き上げる案が出たようです。

2009 ~ 2011年 内閣人事局国家公務員法等改正法案」など 廃案

この時期の国家公務員法改正は、人事院内閣人事局に変更することが焦点だったようです。国家公務員の定年との関係は薄そうです。

2006 ~ 2008年 天下り対策

天下り問題が盛り上がって対策として、「国家公務員制度改革基本法成立」成立しました。

国家公務員制度改革基本法 - Wikipedia

公務員の再就職を一元管理する「官民人材交流センター」を内閣府に設け、公務員にも能力・実績主義を導入し、設置後3年以内に各省庁による天下りのあっせんも全面禁止する

https://www.gyoukaku.go.jp/siryou/koumuin/080613kihonhou_gaiyou.pdf

③ 雇用と年金の接続の重要性に留意して、次の措置を講ずる。 a 定年まで勤務できる環境を整備するとともに、再任用制度の活用の拡大を図る。 b 定年を段階的に六十五歳に引上げることについて検討する。 c aの環境の整備及びbの定年の引上げの検討に際し、高年齢である職員の給与の 抑制を可能とする制度その他のこれらに対応した給与制度の在り方並びに役職定年 制度及び職種別定年制度の導入について検討する。

その中に再任用や定年の引き上げも含まれていました。

RailsConf 2020のKent Beckのトークを見た

koicさん*1を真似ました。

Software design is an exercise in human relationships

我々、プログラマは次のような段階を経て、コミットの粒度を工夫していきます。

  1. 巨大な1つの塊
  2. 構造の変更(リファクタリング)と機能の変更に分ける
  3. 構造の変更(リファクタリング)と機能の変更のいくつもの小さなコミットに分ける
  4. 3を1つのPRから、小さなPRへ分ける

な話を解説していました。 僕個人の体験とも一致していて納得感があります。

Kent Beckらしいと思ったのは、この工夫を「relationshipのメンテナンスだ」と表現しているところです。

Waiter(プロダクトオーナー)とChanger(プログラマ)の人間関係、Changer(プログラマ)とChanger(プログラマ)の人間関係のメンテナンスだと。

なるほど、"XP is about social change."*2のKent Beckでした。

参考:RailsConf 2020

sjisの文章をShift_JISとして読むとEncoding::UndefinedConversionError

Rubyの話です。

‘Ⅸ’(ローマ数字の9) を含むsjisの文章をShift_JISとして読むとエラーが起きます。sjisとして読むとエラーが起きません。

たとえば、

''.encode('sjis', 'utf-8').encode('utf-8', 'Shift_JIS')

はエラーが起きて

''.encode('sjis', 'utf-8').encode('utf-8', 'sjis')

はエラーが起きません。 発生するエラーはEncoding::UndefinedConversionErrorです。

最初、Encoding::UndefinedConversionError は変換先が無いときに出るのだろう、と考えました。 ローマ数字がutf-8に含まれていないのかな?、と考えました。 今どきそんな事ある?と疑いつつ

色々試しているうちに、変換元の文字コード指定の問題(sjisShift_JISか)ということに気が付きました。 厳密なShift JISにはローマ数字が含まれていなくて、CP392のようなWindows拡張のShift JISにはローマ数字が含まれているからです。

そこで「Rubyのどこかに文字コードの変換テーブルがあって、その変換元がないときもEncoding::UndefinedConversionErrorがでる」と仮説を立てました。 どこでしょう?

このへんかなあ?C言語なんもわからん・・・

イベントハンドラーAの処理があるときはイベントハンドラーBの処理を止めたいです

ブラウザのJavaScriptの話です。 イベントハンドラーAの処理があるときはイベントハンドラーBの処理を止めたいです。

イベントハンドラーAは新しいNodeを作って選択します。 イベントハンドラーBは選択を解除します。

期待する動作は、新しいNodeができて選択されてほしいです。今はイベントハンドラーBが発火して、新しいNodeの選択が解除されます。

最初、stopPropagationを使おうと思いましたが、どちらのイベントハンドラーもリッスンしているElementは同じのため、伝播タイミングが同じなので、ダメでした。 とりあえず、イベントハンドラーAでイベントオブジェクトにフラグをたてて、イベントハンドラーBでフラグチェックすれば、期待通りに動作します。 もう少しかっこいい実装方法はないものでしょうか?

  1. イベントオブジェクトではない、何かで状態を持つ
  2. イベントハンドラーAのリッスンするElementを、子Elementにする(で、stopPropagationを使う)
  3. イベントハンドラーBをイベントハンドラーAより先に発火させる

結局2にしました。

rbenvでのバージョン指定が効かなくなりました

~ rbenv version
2.5.5 (set by /Users/shigerunakajima/pubannotation/.ruby-version)
~ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]

ruby のバージョンが上がっている。パスが上書きされたのでしょうか?

~ echo $PATH
/usr/local/opt/libxml2/bin /usr/local/opt/ruby/bin /Users/shigerunakajima/.yarn/bin /Users/shigerunakajima/.rbenv/shims /usr/local/bin /usr/local/bin /usr/bin /bin /usr/local/sbin /usr/sbin /sbin /opt/X11/bin /usr/local/share/dotnet ~/.dotnet/tools /Library/Frameworks/Mono.framework/Versions/Current/Commands /Applications/Wireshark.app/Contents/MacOS

/usr/local/opt/ruby/bin/ruby/Users/shigerunakajima/.rbenv/shims より前にいるのがよくないのでしょうか? /usr/local/opt/ruby/bin/ruby を消してみましょう。

あれ?historyset -U fish_user_paths (string match -v /usr/local/opt/ruby/bin $fish_user_paths) が残っているなあ・・・前にも同じことをしたのかな? 実行してみます。

~ set -U fish_user_paths (string match -v /usr/local/opt/ruby/bin $fish_user_paths)
set: Universal variable 'fish_user_paths' is shadowed by the global variable of the same name.

なぜか怒られます。グローバル変数の fish_user_paths がある?

~/.config/fish/config.fishset -g fish_user_paths "/usr/local/opt/ruby/bin" $fish_user_paths って書いてありました! 消したら

~ rbenv local 2.5.5
~ ruby --version
ruby 2.5.5p157 (2019-03-15 revision 67260) [x86_64-darwin18]

わーい

ただしHomebrewで入れているRubyは使えなくなりました。PATHから /usr/local/opt/ruby/bin 消したんだから、そりゃそうか・・・ set -U fish_user_paths $fish_user_paths /usr/local/opt/ruby/bin したら

~ echo $PATH
/Users/shigerunakajima/.yarn/bin /usr/local/opt/ruby/bin /Users/shigerunakajima/.rbenv/shims /usr/local/bin /usr/local/bin /usr/bin /bin /usr/local/sbin /usr/sbin /sbin /opt/X11/bin /usr/local/share/dotnet ~/.dotnet/tools /Library/Frameworks/Mono.framework/Versions/Current/Commands /Applications/Wireshark.app/Contents/MacOS

rbenvの前に来ちゃった・・・

再起動したら理想的な並び順に変わりました。

~ echo $PATH
/Users/shigerunakajima/.rbenv/shims /usr/local/bin /Users/shigerunakajima/.yarn/bin /usr/local/opt/ruby/bin /usr/local/bin /usr/bin /bin /usr/local/sbin /usr/sbin /sbin /opt/X11/bin /usr/local/share/dotnet ~/.dotnet/tools /Library/Frameworks/Mono.framework/Versions/Current/Commands /Applications/Wireshark.app/Contents/MacOS

なんじゃそれ?

gem install libxml-ruby -v '3.1.0' に失敗します

Building native extensions. This could take a while...
ERROR:  Error installing libxml-ruby:
    ERROR: Failed to build gem native extension.
    current directory: /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/libxml-ruby-3.1.0/ext/libxml
/Users/shigerunakajima/.rbenv/versions/2.5.5/bin/ruby -r ./siteconf20200414-17043-wpqllw.rb extconf.rb
/Users/shigerunakajima/.rbenv/versions/2.5.5/bin/ruby: warning: shebang line ending with \r may cause problems
checking for libxml/xmlversion.h in /opt/include/libxml2,/opt/local/include/libxml2,/usr/local/include/libxml2,/usr/include/libxml2... no
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.
Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/Users/shigerunakajima/.rbenv/versions/2.5.5/bin/$(RUBY_BASE_NAME)
    --with-xml2-config
    --without-xml2-config
    --with-xml2-dir
    --without-xml2-dir
    --with-xml2-include
    --without-xml2-include=${xml2-dir}/include
    --with-xml2-lib
    --without-xml2-lib=${xml2-dir}/lib
 extconf failure: need libxml2.
    Install the library or try one of the following options to extconf.rb:
      --with-xml2-config=/path/to/xml2-config
      --with-xml2-dir=/path/to/libxml2
      --with-xml2-lib=/path/to/libxml2/lib
      --with-xml2-include=/path/to/libxml2/include
To see why this extension failed to compile, please check the mkmf.log which can be found here:
  /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-18/2.5.0/libxml-ruby-3.1.0/mkmf.log
extconf failed, exit code 1
Gem files will remain installed in /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/libxml-ruby-3.1.0 for inspection.
Results logged to /Users/shigerunakajima/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-18/2.5.0/libxml-ruby-3.1.0/gem_make.out

libxml2が見つからないみたいです。

~ brew install libxml2
Warning: libxml2 2.9.10_1 is already installed and up-to-date
To reinstall 2.9.10_1, run `brew reinstall libxml2`

Homebrew的にはインストール済みです。

checking for libxml/xmlversion.h in /opt/include/libxml2,/opt/local/include/libxml2,/usr/local/include/libxml2,/usr/include/libxml2... no

とエラーメッセージが出ています。ヘッダファイル自体はあります。

~ ls /usr/local/opt/libxml2/include/libxml2/libxml/xmlversion.h
/usr/local/opt/libxml2/include/libxml2/libxml/xmlversion.h

ヘッダファイルを探すパスに /usr/local/opt/libxml2/include/libxml2 が入っていないのが問題のようです。

f:id:ledsun:20200508061818p:plain

~ bundle config build.libxml-ruby --with-xml2-config=/usr/local/opt/libxml2/bin/xml2-config
~ bundle config
Settings are listed in order of priority. The top value will be used.
build.libxml-ruby
Set for the current user (/Users/shigerunakajima/.bundle/config): "--with-xml2-config=/usr/local/opt/libxml2/bin/xml2-config"

gem installは失敗します。だってbundle configだもん。 でもbundle installは成功するので、大丈夫でした!

切符を通したときだけ通れる改札口Ractor

前回、Ractor.selectでtakeとrecvを両方待てることがわかりました。 これを使って切符を通したときだけ通れる改札口を作ります。

完成形

キーボード入力を待つRactor = Ractor.new { loop { Ractor.yield gets.chomp } }

改札口Ractor = Ractor.new(キーボード入力を待つRactor) do |キーボード入力|
  ゲート = :閉

  loop do
    # キーボード入力と自身へのメッセージを両方待つ
    メッセージの種類, 受信したメッセージ = Ractor.select キーボード入力, self

    if メッセージの種類 == :recv
      ゲート = :開
    else
      Ractor.yield 受信したメッセージ if ゲート == :開
      ゲート = :閉
    end
  end
end


loop do
  改札口Ractor.send 'に切符を入れる'
  入場者 = 改札口Ractor.take

  p "#{入場者} が入場しました"
  sleep 1
end

実行すると、たくさんキー入力をしても、1秒に一回だけ受け付けます。

感想

Ractor.selectの戻り値の第一引数、自身へのイベントだと:recv帰ってくるのですね。selfが帰ってくるのかと思っていました。

なんだろう、この・・・ES5以前のJavaScript感。懐かしい。クラスを作らないで...

  1. Ractorでスコープを切る
  2. ブロック内のローカル変数で状態を持つ
  3. Ractor.newで定義と同時に、外部への参照を受け取る(実行する)

モジュールパターンやんけ!

var module1 = function(initState){
  var state = initState

  return {
    getState: function(){
      return state
    },
    setState: function(newVal){
      state = newVal
    }  
  }
}('hoge')

みたいな、関数スコープしかなかった時代にプライベートな状態を持つオブジェクトを作るためにやっていた工夫と似ている点を感じました。

Ractor.recvの代わりにRactor.selectする

RactorはsendされたメッセージをRactor.recvで受け取ります。 Ractor.selectでも受け取れます。 例えば、次の例です。

r = Ractor.new do
  # message = Ractor.recv と同じ
  _, message = Ractor.select self
  p "Hello #{message}!"
end

r.send 'World'
r.take

実行するとsendされた'World'を結合して、Hello World!を出力します。

Ractor.selectでsendされるメッセージを待てると何がうれしいのでしょうか?

別のRactorをtakeしているときに、自身にsendされるメッセージと別のRactorからのtakeを同時に待てます。 例えば、次の例です。

r1 = Ractor.new do
  Ractor.yield 'r1がyieldしたメッセージ'
  'r1がreturnしたメッセージ'
end

r2 = Ractor.new(r1) do |r1|
  loop do
    # takeとrecvを同時に待つ
    _, message = Ractor.select self, r1
    p message
  end
end

r2.send 'r2にsendしたメッセージ1'
r2.send 'r2にsendしたメッセージ2'

r2.take

実行すると次のように表示されます。

~ ruby select_take_and_recv.rb
"r2にsendしたメッセージ1"
"r2にsendしたメッセージ2"
"r1がyieldしたメッセージ"
"r1がreturnしたメッセージ"

歌うRactor

Ractorで歌ってみましょう。

歌う部分はRactorは関係ありません。出落ちです。 Ractor内で、spawnしたプロセスをメインスレッドでkillできるか確認します。

非同期なspawn関数をRactor内で実行する必要があるのかは知りません。

完成形

LYRICS =<<FIN
ちょうちょう ちょうちょう 菜の葉にとまれ
菜の葉にあいたら 桜にとまれ
桜の花の 花から花へ
とまれよ遊べ 遊べよとまれ
FIN
.gsub("\n", ' ')
.freeze

pid = Ractor.new { spawn "say #{LYRICS}" }.take

sleep 10

Process.kill 9, pid

pidは数値なので、Ractorと共有可能です。

共有可能なオブジェクト

文字列は共有可能なオブジェクトではありません。 Ractorから参照するとNameErrorが起きます。 freezeして共有可能なオブジェクトにします。

Ractor間のメッセージ送受信 Pull型とPush型

やることは前回と同じです。Ractorの構成を組み替えてみました。

Pull型とPush型

Ractorのメッセージ送受信にはPull型とPush型があります。 前回、メインスレッド(?)でRactorインスタンスをselectしてイベントを待ちました。Pull型です。

f:id:ledsun:20200422213039p:plain
Pull型

今回、描画用のRactorインスタンスrendererを用意して、他のRactorインスタンスからイベントをsendします。Push型です。

f:id:ledsun:20200422213126p:plain
Push型

完成形

require 'io/console'

# 結果を描画するRactor。inputとclockの存在を知りません。
renderer = Ractor.new do
  val = 0
  loop do
    # メッセージを待ちます
    msg = Ractor.recv

    # メッセージがユーザー入力だったら値をリセットします。
    val = 0 if msg == :reset

    # カウントアップします。
    val += 1
    # 行をクリア
    print "\e[2K"
    # 行頭へ移動
    print "\e[0G"
    # 出力
    print val
  end
end

# ユーザー入力を待つRactor。引数でrendererを受け取ります。
input = Ractor.new renderer do |renderer|
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    # メッセージを送ります
    renderer << :reset
  end
end

# クロックイベントを発生するRactor。引数でrendererを受け取ります。
Ractor.new renderer do |renderer|
  loop do
    # メッセージを送ります
    renderer << nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# Ractorの終了を待つ?
Ractor.recv

はまったところ

Ractor.recvを書かずに実行したら、何も出力されずにプログラムが終了しました。 Ractorインスタンスが動き出す前に、メインスレッド(?)が終了したようです。

感想

Pull型とPush型では、Ractor間の依存関係が逆転しました。 この規模では、ふーんて感じです。

Ractorを使ったTUIプログラムの一歩

前回のユーザー入力とクロックを両方待つプログラムを少し改造します。

  • Enter以外の入力を受け取る
  • カーソルを移動せずに書き換える

後者はRactorは関係ないです。

Enter以外の入力を受け取る

任意の1文字を受け取るためにSTDIN.getchを使いたいです。 STDINは共有不可能オブジェクトです。 次のようなRactorを書くとcan not access non-sharable objects in constant STDIN by non-main Ractors (NameError)が発生します。

# ユーザー入力を待つRactor
input = Ractor.new do
  while  "\C-c" != STDIN.getch
    Ractor.yield :reset
  end
end

共有不可能オブジェクトをRactorに渡すときはmoveします。

# ユーザー入力を待つRactor
input = Ractor.new do
  io = Ractor.recv
  while  "\C-c" != io.getch
    Ractor.yield :reset
  end
end

input.send STDIN, move: true

カーソルを移動せずに書き換える

エスケープシーケンスを使ってカーソルを操作します。

  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val

完成形

require 'io/console'

# ユーザー入力を待つRactor
input = Ractor.new do
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val
end

次のように動きます。一定のペースでカウントアップし、何か入力すると1に戻ります。

f:id:ledsun:20200421225620g:plain
プログラム実行時のスクリーンショット

参考

Ractorを試す 改訂版

macOShttps://github.com/ko1/ruby/tree/ractorソースコードをビルドする方法を改善しました。

インストール先

コンパイルしたRubyをmake installしたら「PCの環境が壊れるかも」とビビっていたところ、 id:hanachin さんに ./configure --prefix="$HOME/.rbenv/versions/ractor" するとrbenvの1実行環境として扱えると教えてもらいました。

OpenSSL

makeのログを見てたら、次のようなエラーも出ていました。

*** Following extensions are not compiled:
openssl:
    Could not be configured. It will not be installed.
    /Users/shigerunakajima/ractor/ext/openssl/extconf.rb:99: OpenSSL library could not be found. You might want to use --with-openssl-dir=<dir> option to specify the prefix where OpenSSL is installed.
    Check ext/openssl/mkmf.log for more details.
*** Fix the problems, then remove these directories and try again if you want.

--with-openssl-dir=/usr/local/opt/openssl@1.1/オプションをつけて、HomebrewでインストールしたOpenSSLのディレクトリを指定します。

完成形

CFLAGS=-Wno-error=shorten-64-to-32 ./configure --prefix="$HOME/.rbenv/versions/ractor" --with-openssl-dir=/usr/local/opt/openssl@1.1/
make
make install

まだ、次のようなエラーが表示されます。

installing bundled gems:            /Users/shigerunakajima/.rbenv/versions/ractor/lib/ruby/gems/2.8.0
Traceback (most recent call last):
    6: from ./tool/rbinstall.rb:957:in `<main>'
    5: from ./tool/rbinstall.rb:957:in `each'
    4: from ./tool/rbinstall.rb:960:in `block in <main>'
    3: from ./tool/rbinstall.rb:889:in `block in <main>'
    2: from ./tool/rbinstall.rb:889:in `foreach'
    1: from ./tool/rbinstall.rb:894:in `block (2 levels) in <main>'
./tool/rbinstall.rb:818:in `load_gemspec': [/Users/shigerunakajima/ractor/.bundle/gems/minitest-5.14.0/minitest.gemspec] isn't a Gem::Specification (NilClass instead). (TypeError)
make: *** [do-install-all] Error 1

Rubyコマンドのインストールはできているみたいです。

~ ls ~/.rbenv/versions/ractor/bin/ruby
/Users/shigerunakajima/.rbenv/versions/ractor/bin/ruby*

プログラムの実行だけなら、minitestはなくてもよさそうなので、先に進みます。

動作確認

システムのRubyではRactorを使ったRubyスクリプトの実行に失敗します。

~ ruby --version
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin18]
~ ruby sum_1_to_10.rb
Traceback (most recent call last):
sum_1_to_10.rb:2:in `<main>': uninitialized constant Ractor (NameError)

ractor版を使うと実行できました。

~ rbenv shell ractor
~ ruby --version
ruby 2.8.0dev (2020-04-08T16:31:17Z ractor 2658fc668d) [x86_64-darwin18]
~ ruby sum_1_to_10.rb
55

Ractorをつかうプログラムの練習 2つのイベントを待つ

Ractorをつかって、ユーザー入力とクロックを両方待つプログラムを書いてみます。 練習中であり、Ractorを使ったイケているソースコードではありません。

次のプログラムを実装します。 1秒毎にカウントアップします。 ユーザーがEnterキーを押したらカウンターをリセットします。

完成形

# ユーザー入力を待つRactor
input = Ractor.new do
  loop do
    gets
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 1
  end
end

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  p val
end

失敗作

処女作

ユーザー入力があったときだけ、カウンターから帰ってくる値を0に戻さそうと思って書きました。

input = Ractor.new do
  loop do
    gets
    Ractor.yield 0
  end
end

counter = Ractor.new do
  loop do
    Ractor.yield Ractor.recv + 1
    sleep 1
  end
end

counter << 0
loop do
  _, v = Ractor.select input, counter
  p v
  counter << v
end

実際はこんな感じにうごきます。

~ docker run --rm -it -v (pwd):/ractor wakaba260/ruby-ractor-dev ruby ractor/input_and_timer.rb
1
2

0
3
1
4

リセットされた値とされていない値が両方出力されます。 Ractorはキューを持っているので、counter << vした値は上書きされることなく保存されています。

debounceを実装

最新の値だけがほしいので、debounceが実装できれば良さそうです。

input = Ractor.new do
  loop do
    gets
    Ractor.yield 0
  end
end

counter = Ractor.new do
  v = 0
  loop do

    v = Ractor.recv
    Ractor.yield v + 1
    sleep 1
  end
end

debounce = Ractor.new do
  prev = Time.now.to_i
  loop do
    v = Ractor.recv

    now = Time.now.to_i
    if now - prev > 0.1
      p v
      prev = now
    end
  end
end

counter << 0
debounce << 0
loop do
  _, v = Ractor.select input, counter
  counter << v
  debounce << v
end

実際には、これも期待通りには動きません。 毎回counter << vしているので、counterのキューに全部の値が貯まります。

どうやらcounterで1秒に1加算しているのがよくなさそうです。 counterを1秒毎にイベントを発火するclockにし、mainで加算しました。

感想

Ractorインスタンスからグローバルな値を変更できないので、Ractor.yieldを使って値を親に返す必要があります。 その代わり、レールに乗りさせすれば、Ractor.selectを使って、イベントの待ち合わせを簡単に書けます。

スレッドで書くと次のようになります。

queue = Queue.new

input = Thread.new do
  loop do
    gets
    queue << :reset
  end
end

clock = Thread.new do
  loop do
    queue << nil
    sleep 1
  end
end

val = 0
loop do
  flag = queue.pop
  val = 0 if flag == :reset
  val += 1
  p val
end

今回の使い方からは「最初からキューを持っているスレッド」というイメージを持ちました。

Ractorを使ってプログラムを書く練習    

注意:Ractorならではとか、計算効率の良さとか考えていません。

1から10までを足す

# 最終結果送り先のRactor
CR = Ractor.current
r = Ractor.new { CR << Ractor.recv }

1.upto(10) do |i|
  # 数を足すRactorを作ってポインタを置き換える
  r = Ractor.new(r, i) do |next_r, i|
    # 担当の整数を足して次のRactorに送る
    next_r << Ractor.recv + i
  end
end

r << 0 # 計算開始。初期値を送る
p Ractor.recv # 結果を出力

<<でRactorインスタンスに値を送ります。 Ractor.recvで値を受け取ります。 並列に動いていません。

10個のRactorを並列に動かす

# 10個のRactorを作る
ractor_list = (1..10).map do |i|
  Ractor.new i do |i|
    i
  end
end

# 10個のRactorの終わりを待つ
until ractor_list.empty?
  # いずれかのRactorの結果を待つ
  r, val = Ractor.select(*ractor_list)
  p val

  # 実行の終わったRactorは、もう待たない
  ractor_list.delete r
end

Ractor.selectでRactorインスタンスの終了を待ちます。 Ractor.newに渡したブロックの戻り値が得られます。

参考

https://github.com/ko1/ruby/blob/ractor/ractor.ja.md