@ledsun blog

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

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

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