Ractorをつかって、ユーザー入力とクロックを両方待つプログラムを書いてみます。
練習中であり、Ractorを使ったイケているソースコードではありません。
次のプログラムを実装します。
1秒毎にカウントアップします。
ユーザーがEnterキーを押したらカウンターをリセットします。
完成形
input = Ractor.new do
loop do
gets
Ractor.yield :reset
end
end
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
今回の使い方からは「最初からキューを持っているスレッド」というイメージを持ちました。