RubyのMethod探索の話
先日参加させていただいた Ruby Meetup Kansai で RailsのActiveRecordのソースを追っかけててどうしても ActiveRecord::Core.generated_feature_methods が何しているかわからないと相談させていただくと、なんとその日中に調べてブログにアップしていただきました。 kennyjのブログ(仮)
感謝、感謝。
ActiveRecord::Core.generated_feature_methods が何をしているかは kennyjさんのブログを参照ということにして、MixInの使い方がおもしろかったので、備忘録をかねてメモ。
今までは include した時点で Module にあるMethod が include した側に足されるイメージだったけど、どうやら違うみたいで、includeしたModuleがメソッド探索に含まれるのが正解みたい。
class Sample def self.before @before ||= begin mod = const_set(:Before, Module.new) include mod mod end end def self.mixin @mixin ||= begin mod = const_set(:Mixin, Module.new) include mod mod end end end Sample.before Sample.mixin Sample.mixin.module_eval { define_method(:abc) { "mixin" } } Sample.new.abc # => "mixin" # 同名のMethodをBeforeに定義しても後からincludeされた # Mixinの方がメソッド探索上優先されるのでそちらが呼ばれる。 Sample.before.module_eval { define_method(:abc) { "before" } } Sample.new.abc # => "mixin"
おそらく自分が使う事は無いと思うけど、Gemのソース読むときに役立つことがあるかも。
Ruby の Monitor と ConditionVariable の使い方
Ruby の Monitor と ConditionVariable の使い方
なかなかぐぐっても日本語の資料が見つからなかったので自分で動かしてみた。(ぐぐる能力低い)
まずThreadの直列化
require "thread" require "monitor" moni = Monitor.new val = 0 Thread.new { 3.times { puts "thread1 start: #{val}" val+=1 sleep 0.1 puts "thread1 end: #{val}" } } Thread.new { 3.times { puts "thread2 start: #{val}" val+=1 sleep 0.1 puts "thread2 end: #{val}" } } sleep
結果は以下の通り。
start > end, start > end と一つ一つ処理してほしいのに途中でまざってしまってる。
thread1 start: 0 thread1 end: 1 thread1 start: 1 # もうココでthread2が割り込んでしまっている。。 thread2 start: 1 thread2 end: 3thread1 end: 3 thread1 start: 3 thread2 start: 4 thread1 end: 5 thread2 end: 5 thread2 start: 5 thread2 end: 6
synchronizeをつけて再度実行
require "thread" require "monitor" moni = Monitor.new val = 0 Thread.new { moni.synchronize { 3.times { puts "thread1 start: #{val}" val+=1 sleep 0.1 puts "thread1 end: #{val}" } } } Thread.new { sleep 0.1 moni.synchronize { 3.times { puts "thread2 start: #{val}" val+=1 sleep 0.1 puts "thread2 end: #{val}" } } } sleep
thread1 start: 0 thread1 end: 1 thread1 start: 1 thread1 end: 2 thread1 start: 2 thread1 end: 3 thread2 start: 3 thread2 end: 4 thread2 start: 4 thread2 end: 5 thread2 start: 5 thread2 end: 6
うまくいったヾ(@^▽^@)ノ
Thread同士のまちあわせ。
Thread同士の処理を待ち合わせるのにループでポールしてもいいんだけど、そういうプログラムって往々にして扱いにくい(気がする)のでモダンなやり方を調べてみた。
require "thread" require "monitor" moni = Monitor.new # thread同士の待ち合わせには ConditionVariableというのを使う # Monitor#new_cond でそのMonitorにひもづいている # ConditionVariable のインスタンスを取得する事ができる cond = moni.new_cond thread1 = Thread.new { moni.synchronize { # ConditionVariable#waitで待ち合わせる。 # 必ずロックを取得した thread でしか wait は呼べない。 # つまり以下の二つの状態でした wait は呼べない # ・Monitor#enter でロック取得した以降 Monitor#exit でロックを解放するまで # ・Monitor#synchronize ブロックの中。(このサンプルだとこっち) # # waitを呼ばれた thread は一度 lock を解放し thread を sleep させ # 他の Thread に処理を移す。 cond.wait # 起こされたここから処理が再開する。 puts "receive" } } thread2 = Thread.new { sleep 1 moni.synchronize { # condで待ってる他のthreadを起こす。 cond.signal } } thread1.join; thread2.join
上記の応用として ThreadPoolをつくってみた
class ThreadPool # 渡されたブロックをThreadで処理する def initialize(size=5, &block) @threads = [] # Threadの格納しておく配列 @jobs = [] # blockに渡す引数を格納し、Queueの役割をする。 @monit = Monitor.new @cond = @monit.new_cond @block = block # 実行される処理 @size = size # 最大Thread数 end def run @size.times do spawn_thread end end def run_thread # synchronize抜けてから処理を実行しないと # Threadを使った並列処理ができない。 # @jobsは全てのthreadで共通のスコープの変数なので # synchronizeの外で触るとくちゃくちゃになるので # いったんThread固有のローカル変数に格納して # このローカル変数を引数にスレッドブロックを呼ぶ。 job = nil loop do @monit.synchronize do # signalを受け取るたびに block を評価して # 真になれば続きの処理を行い、偽であれば再度スリープする。 @cond.wait_until { @jobs.size > 0 } # 真になったthreadのみココにくる job = @jobs.pop end @block.call(job) end end def spawn_thread @monit.synchronize do @threads << Thread.new(&method(:run_thread)) end end def <<(job) @monit.synchronize do # jobにキューを格納しThreadを一つ起こす @jobs << job @cond.signal end end end # Threadを5つ作り、スレッドが実行するブロックを渡す。 pool = ThreadPool.new(5) do |arg| p arg # heavy process sleep 3 end # Threadを準備 pool.run # 10回キューする 10.times do pool << "aa" end sleep
あとは、Thread止める処理とか必要だけどまた今度(・o・)ゞ
参考: pumaのThreadPool
西脇.rb & 東灘.rb(第3回) SPDYでRails動かすまで + Rubyのthread調べた
もくもく会第3回いってきました。
今回のお題は2つ
なぜ二つになったかというとSPDY調べていくうちに
Rails(ruby) 関係ないやん( ̄Д ̄;)
となったため急遽追加。
Mac(lion) で SPDYでRails動かすまで
nginxが1.4よりSPDYが標準装備となったらしい http://nginx.org/en/CHANGES-1.4
これはもらった!
APサーバーはunicornでもよかったが、自分はネコ科の動物には目がないので nginx + puma でいくことにする。
Nginxをコンパイル。
brewにあるやつみても 1.4.0 ではなさそうなので、ソースを落としてくる。
cd work/ wget http://nginx.org/download/nginx-1.4.0.tar.gz tar zxvf nginx-1.4.0.tar.gz
色々調べるとSSLも最新でないとダメみたい。
Mac(Lion)の標準搭載はだいぶ古いのでこれまたソースをダウンロードしてくる。
NginxのconfigureでOpenSSLのソースあるところを指定するとそのまま取り込んでくれるらしい
べんり〜
cd work/ngix-1.4.0 wget http://www.openssl.org/source/openssl-1.0.1e.tar.gz tar zxvf openssl-1.0.1e.tar.gz
configureはちょっと特殊。
Macで運用するわけではないし、うまくいかなかったときにversion複数さくさく切り替えれると楽かなと適当なところにbinary置く事にした。
あとでbrewで入れるかもしれないし。
最後のオプションでOpenSSLのソースを展開したディレクトリを指定。
./configure \ --prefix=$HOME/work/nginx \ --sbin-path=$HOME/work/nginx/sbin \ --with-http_spdy_module \ --conf-path=$HOME/work/nginx/etc/nginx.conf \ --error-log-path=$HOME/work/nginx/var/error.log \ --http-log-path=$HOME/work/nginx/var/access.log \ --http-client-body-temp-path=$HOME/work/nginx/tmp/client_body \ --http-proxy-temp-path=$HOME/work/nginx/tmp/proxy \ --http-fastcgi-temp-path=$HOME/work/nginx/tmp/fastcgi \ --http-uwsgi-temp-path=$HOME/work/nginx/tmp/uwsgi \ --http-scgi-temp-path=$HOME/work/nginx/tmp/scgi \ --pid-path=$HOME/work/nginx/tmp/nginx.pid \ --lock-path=$HOME/work/nginx/var/nginx \ --with-http_ssl_module \ --with-openssl=./openssl-1.0.1e
ここでエラー
./configure: error: the HTTP rewrite module require the PCRE library. ...
rewrite はおそらく使わないけどとりあえず入れる事にする。
sudo brew install pcre
そしてmake
make
んで、またエラー
WARNING! If you wish to build 64-bit library, then you have to invoke './Configure darwin64-x86_64-cc' *manually* You have about 5 seconds to press Ctrl-C to abort.
いろいろ調べたら OpenSSL の話だった。
んで、configure の中身読みながら以下をconfigureに追加
--with-openssl-opt=darwin64-x86_64-cc
するも撃沈。
いくら調べてもよくわからない(Shellがたいして読めない)んで直接書き換える。
diff -u auto/lib/openssl/make.org auto/lib/openssl/make --- make.org 2013-05-06 21:21:58.000000000 +0900 +++ make 2013-05-06 21:19:42.000000000 +0900 @@ -56,7 +56,7 @@ $OPENSSL/.openssl/include/openssl/ssl.h: $NGX_MAKEFILE cd $OPENSSL \\ && \$(MAKE) clean \\ - && ./config --prefix=$ngx_prefix no-shared $OPENSSL_OPT \\ + && ./Configure darwin64-x86_64-cc --prefix=$ngx_prefix no-shared $OPENSSL_OPT \\ && \$(MAKE) \\ && \$(MAKE) install LIBDIR=lib
make
成功ヾ(@^▽^@)ノ
証明書は以前練習で発行したのを使用。
最後にコンフィグの設定。デフォルトのやつをコピーして見よう見まねで変更
# ここに puma と通信する UnixSocket の path を指定 upstream app { server unix:///var/run/app.sock; } server { # listenポートを変えてssl spdy と追加 listen 3000 default ssl spdy; server_name localhost; # SSLの設定を追加 ssl on; ssl_certificate /path/to/your_cert.cert; ssl_certificate_key /path/to/your_secret.key; ssl_session_timeout 5m; root /path/to/app/public; location / { # root html; # index index.html index.htm; # ここでupstream指定 proxy_pass http://app; # 多分Headerに元のHostを足してる? proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } #error_page 404 /404.html; ....
Railsを準備
まずはインストールするフォルダを作成
mkdir app cd app
次にGemfileを作成。今回はせっかくなので一番新しいのを使った。
source "https://rubygems.org" gem "rails", "4.0.0.rc1"
rails をインストール
bundle install --path vendor/bundle
Railsの基本ファイルを生成。
rails new .
上でGemfileが上書きされるので、上書きされたGemfileにpumaを追加
gem "puma"
puma インストール
bundle
puma確認
bundle exec rails s puma => Booting Puma => Rails 4.0.0.rc1 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Ctrl-C to shutdown server Puma 2.0.1 starting... * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://0.0.0.0:3000
最後にSPDY確認
こちら(spdy indicator)のchromeエクテンションで確認。
nginx起動。
sbin/nginx
puma 起動
# -bでさっきnginxで指定した UnixSocket のpathを指定 bundle exec puma -b unix:///var/run/app.sock
ブラウザで確認
RubyのThreadについてしらべる
RubyにはGVL(Giant VM lock)なるものがあるらしくどういうものなのってことで、調べたかったのは以下の5点
- CPU100%以上ちゃんと使うの?
- switchのタイミングは?
- Monitor の便利な使い方
- Thread.currentの使いどころ
- Abort on Exceptionの使いどころ
んで、時間内に調べられたのは上2つ
CPU100%以上ちゃんと使うの?
公式のDocより引用
ネイティブスレッドを用いて実装されていますが、 現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行される ネイティブスレッドは常にひとつです。 ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。その場合にはスレッドは同時に実行され得ます。 また拡張ライブラリから GVL を操作できるので、複数のスレッドを 同時に実行するような拡張ライブラリは作成可能です。
同時に実行されるネイティブスレッドは常にひとつです。
(・_・?)...ン?
じゃあCPU100%しか使わないの?
def fib(n) n < 2 ? n : fib(n-1) + fib(n - 2) end th1 = Thread.new { sleep 1 100.times { fib(1000) } } th2 = Thread.new { sleep 1 100.times { fib(1000) } } th1.join; th2.join;
ちゃんとつかってる!理由はわからないけど\(^o^)/
switchのタイミングは?
IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。
さっぱりわからないのでぐぐってみるととてもよいスライドを見つける
http://www.slideshare.net/kosaki55tea/ruby-gvlimprovement-8617719
全ては理解できなかったけど、無理矢理理解すると
- IOのようなwaitが発生しそうな処理のときにswitchする。たぶんsleepもだとおもう
- 長い時間同じthreadが走らないようにTimerでswitchする。これは必ず同じthreadがもう一度Lockをとらないようにしている。
ふむふむ。ならこれでどう?
th1 = Thread.new { sleep 1 30.times { $th1 << $global $global += 1 w.write("..") } } th2 = Thread.new { sleep 1 30.times { $th2 << $global $global += 1 r.read(1) } } th1.join; th2.join p $th1 p "------------" p "------------" p "------------" p $th2
きれいには切り替わらないな \(^o^)/
この辺で時間終了。
レビュー中(発表中) に Unixの仕組みと似ているという大変貴重な意見をいただけたので、時間できたときにもう少しCのRubyのソース追ったりして調べてみようっと。
その後の懇談会にて、こんなぐだぐだな発表に関わらずMVPをいただけたヾ(@^▽^@)ノ
これが何か良くわかってないけど、プリペイドカードっぽいのでスターバックスいって支払いのときに出してみて店員の反応を見て考えようw
今回は書く事多くてつかれたw
西脇.rb & 東灘.rb(第2回)でつくってみたいもの(結果)
感想
いくまではいつものごとく人見知りすぎてお腹いたくなったけど、行ってみたら色々しゃべれて楽しかった。
特に同じ悩みを持つ人としゃべれてよかった。
なかなかもくもく会おすすめ。
あと、やっぱり意識高い人はセンスがいいなと思った。目的もって勉強するのとやみくもに勉強するのでは大分違うんだなと感じた。僕もガンバラナイト
今日やったことまとめ
前回書いたやつ分はおおむね実装できた感じ。
Gem https://rubygems.org/gems/mameconf
github https://rubygems.org/gems/mameconf
一応Gem化したのでご興味ある方はどうぞつかってみて下さいw
git で戻しながら今日やったことの復習
初期バージョン
mameconf.rb
require "mameconf/version" module Mameconf def self.included(base) # include で 特異メソッド足す為にhookでextend # ClassMethods つくって足すのはもう古い? base.extend ClassMethods end module ClassMethods # アクセッサ足すときに呼ばれる特異メソッド def mameconf(name, options={}) # デフォルト値をローカル変数にセット。正直いらない default_value = options[:default] # memeconfを呼ばれたときにクラスコンテキストを開いてインスタンスメソッドを定義する。 # ヒアドキュメントでRUBYって使うのはRailsから拝借 # default_value のinspectをつかっているのは # デフォルト値が Symbol なら :sym # 文字列なら "string" 数値なら 123 と文字列展開 # されるため、今回の用途に都合がよいから。 class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} @#{name} ||= #{default_value.inspect} end def #{name}=(val) @#{name} = val end RUBY end end end
mameconf_spec.rb
require "spec_helper" describe Mameconf do # テスト対象になるクラス class Included include Mameconf end describe "defualt value" do subject do # 実際にincludeしてincludeしたクラスを返す Included.class_eval do mameconf :host, default: "localhost" end Included end it "returns default valeu if not present" do subject.new.host.should eq "localhost" end it "allows override default value" do instance = subject.new instance.host = "google.com" instance.host.should eq "google.com" end end end
to_hashメソッドを追加ver
module ClassMethods # mameconf されたattributeを保持しておくためのアクセサ # インスタンス変数にじゃなくてアクセさにしたのは # インスタンスから self.class.** で呼べるようにするため。 + attr_accessor :mameconf_attr_names + + def mameconf_attr_names + @mameconf_attr_names ||= [] + end + def mameconf(name, options={}) + mameconf_attr_names << name + default_value = options[:default] class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} @@ -16,6 +24,15 @@ module Mameconf def #{name}=(val) @#{name} = val end + + def to_hash + ret = {} + self.class.mameconf_attr_names.each do |attr| + attr_sym = attr.to_sym + ret[attr_sym] = self.__send__(attr_sym) + end + ret + end RUBY end end
describe Mameconf do - class Included - include Mameconf - end - describe "defualt value" do subject do # うっ。インスタンスメソッド足すので毎回afterで消さないといけない事に気づく # 寿命をitに限定させるために Class.new 使う事にする - Included.class_eval do + Class.new do + include Mameconf mameconf :host, default: "localhost" end - Included end it "returns default valeu if not present" do subject.new.host.should eq "localhost" end - it "allows override default value" do # to いるんじゃない?英語得意じゃないのでわからない。。 + it "allows to override default value" do instance = subject.new instance.host = "google.com" instance.host.should eq "google.com" end end + + describe "#to_hash" do + subject do + Class.new do + include Mameconf + + mameconf :host, default: "localhost" + mameconf :port, default: 3337 + end.new + end + + it "returns hash that included default values" do + subject.to_hash.should eq ({ host: "localhost", port: 3337 }) + end + end end
継承したケースのテスト追加
今回は最初から想定していたので、class variable 使わずに実装したのでテストのみ。
+ describe "#inheritance" do + before do + @parent = Class.new do + include Mameconf + + mameconf :host, default: "localhost" + end + + @sub = Class.new(@parent) + end + + it "has mameconf attribute" do + @sub.new.host.should eq "localhost" + end + + context "add new attribute on Sub Class" do + before do + @sub.class_eval do + mameconf :sub, default: "inu" + end + end + + it "has different memory space" do + @sub.new.sub.should eq "inu" + # わざわざ 例外のテストしなくても respond_to? テストするだけでよさそう。 # まっちゃも用意されてそう + expect { + @parent.new.sub + }.to raise_error NoMethodError + end + end + end
nilをセット出来るようにする。
ここまで書いててセッターの作りが甘くて nil がセット出来ない事に気づく。
# nil だと毎回デフォルト値セットしてしまう。。。 "@#{name} ||= #{default_value.inspect}"
セッター呼ばれる時点でフラグ持たしてもいいけど、できたら
@abc = "aa" とかされたときにも対応したい。
色々考えて以下みたいにした
class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name} - @#{name} ||= #{default_value.inspect} # 一度でもインスタンス変数として設定されていれば初期化しない + if !instance_variables.include?(:@#{name}) + @#{name} ||= #{default_value.inspect} + end + + @#{name} end
うまく動くかはもう少し使わないとわからない。
initializerを足す
module Mameconf def self.included(base) base.extend ClassMethods + base.__send__(:include, InstanceMethods) + end + + module InstanceMethods + def initialize_mameconf(options={}) + options.each do |key, value| + method = "#{key}=" + + __send__(method, value) if respond_to?(method) + end + end + + def initialize(options={}) + initialize_mameconf(options) + end end
+ + describe "#initialize" do + subject do + Class.new do + include Mameconf + + mameconf :host, default: "localhost" + end + end + + it "can initiate from constructor" do + subject.new(host: "sibainu.com").host.should eq "sibainu.com" + end + + context "override initializer on Sub Class" do + before do + subject.class_eval do + def initialize(options) + # TODO: 本当はココになにも書かず初期化したい。 + # でも多分無理。 + initialize_mameconf(options) + end + end + end + + it "can initiate from constructor" do + subject.new(host: "sibainu.com").host.should eq "sibainu.com" + end + end + end end
足したけど、やっぱりここでSub Class制限なしを実現できなかった。。。多分無理そうかな。。。
Review で tap 教えてもらいその場でリファクタ
def to_hash - ret = {} - self.class.mameconf_attr_names.each do |attr| - attr_sym = attr.to_sym - ret[attr_sym] = self.__send__(attr_sym) - end - ret + {}.tap {|ret| + self.class.mameconf_attr_names.each do |attr| + attr_sym = attr.to_sym + ret[attr_sym] = self.__send__(attr_sym) + end + } end
おぉ。ret とか宣言するのださいなと思いつつ書いてたのでいいこと教えてもらった(`・ω・´)
総評
すごい人のプレゼン聞くのも勉強になるけど、やっぱアウトプット楽しいし勉強になる。
もくもく会おすすめ(しつこいw)
西脇.rb & 東灘.rb(第2回)でつくってみたいもの
西脇.rb & 東灘.rb(第2回)でつくってみたいもの
include すると default 値などが設定できるattr_accessorの拡張見たいな機能が追加される Mixin
仕様
基本は下のような感じ
class Some include Configable define_config :host, default: "localhost" define_config :port, default: 3000 end Some.new.host #=> "localhost"
そんで、できれば以下の様にコンストラクタで初期化できるようにしたい。
できるなら、サブクラスのコンストラクタに制限なしで。
Some.new(host: "google.com").host #=> "google.com"
そんで、できれば to_hash メソッドが欲しい
Some.new.to_hash #=> { host: "localhost", port: 3000 }
そんで、できれば継承できるようにしたい。
class Sub < Some end Sub.new.host #=> "localhost"
Sinatra で twitter bootstrap v2.3.0
Sinatra で bootstrap 使おうとしてはまったのでめも。
たぶん今しか使えない情報。
へたれなので現時点の最新のdocumentが使える 2.3 をゲット
https://github.com/twitter/bootstrapをcloneして、
git checkout -b v2.3.0
で 2.3に
それぞれの js, less, img フォルダをsinatraでpublic_folderに設定したところの下にコピーしてくる。
自分は下のようにした
./public/js/bootstrap ./public/css/bootstrap ./public/img/bootstrap
overrideするlessを置く
自分は./public/css/override.lessに置いた
// import bootstrap @import "./bootstrap/bootstrap.less"; // change path to image. @iconSpritePath: "../img/bootstrap/glyphicons-halflings.png"; @iconWhiteSpritePath: "../img/bootstrap/glyphicons-halflings-white.png";
まず普通にGemfileに追加
source :rubygems gem "sinatra" gem "less"
それっぽくかいてみる
某掲示板のanswerをそのまま書いてみる。
require 'less' require 'sinatra/base' class App < Sinatra::Base # Make LESS @import statements work Less.paths << settings.views # Use LESS for CSS get '/stylesheets/:style.css' do less(params[:style].to_sym) end end
しかしエラー
pathが見つからない云々いわれる。
いろいろ試しながらソースよんでいくと、cssディレクトリにoverride.less置くには直接viewsオプションを指定しないといけないっぽ。
get "/css/:style.css" do less(params[:style].to_sym, views: "#{dirname}/web/public/css") end
いけた、、とおもってアクセスすると
Less::Error - Cannot call method 'charAt' of undefined
。。。わからん( ̄Д ̄;)
よくよく調べてみると、bootstrap 2.3 で使われている less のversionにless.rbが対応してないっぽい
対応されているこのコミットのverをありがたく使わしていただくことにする。
gem "less", git: "https://github.com/populr/less.rb", branch: "v2.2.2-less1.3.3", submodules: true
submodules: true はなぜか(gitリポジトリを直接指定しているから?)だと submoduleのjs(JSのless)をdlしてこなかったから。
これでやっとみれた!ヾ(@^▽^@)ノ