Skip to content

Instantly share code, notes, and snippets.

@ileitch
Created December 11, 2011 11:08
Show Gist options
  • Save ileitch/1459987 to your computer and use it in GitHub Desktop.
Save ileitch/1459987 to your computer and use it in GitHub Desktop.
Interruptible sleep in Ruby
module InterruptibleSleep
def interruptible_sleep(seconds)
@_sleep_check, @_sleep_interrupt = IO.pipe
IO.select([@_sleep_check], nil, nil, seconds)
end
def interrupt_sleep
@_sleep_interrupt.close if @_sleep_interrupt
end
end
class Test
include InterruptibleSleep
def start
puts "start"
interruptible_sleep 10
puts "sleep interrupted!"
end
def stop
puts "stop"
interrupt_sleep
end
end
test = Test.new
Signal.trap('SIGINT') { test.stop }
test.start
@adis-io
Copy link

adis-io commented Apr 7, 2017

My slightly modified implementation.

class InterruptibleSleep
  def sleep(seconds)
    @_sleep_check, @_sleep_interrupt = IO.pipe
    IO.select([@_sleep_check], nil, nil, seconds)
  end

  def wakeup
    @_sleep_interrupt.close if @_sleep_interrupt && !@_sleep_interrupt.closed?
  end
end

Could somebody say is it thread safe or not?

Could I use it in such way:

class PollerRunner
  def initialize
    @pollers = []
    @sleeper = InterruptibleSleep.new
  end

  def add_poller(poller, timeout = 1)
    @pollers << { poller: poller, timeout: timeout }
  end

  def run
    threads = []
    @pollers.each { |poller| threads << create_thread(poller[:poller], poller[:timeout]) }
    threads.each(&:join)
  end

  private

  def create_thread(poller, timeout)
    Thread.new do
      loop do
        poller.run
        @sleeper.sleep(timeout)
      end
    end
  end
end

@adis-io
Copy link

adis-io commented Apr 7, 2017

In case if somebody wants to stop threads:

class InterruptibleSleep
  def sleep(seconds)
    @_sleep_check, @_sleep_interrupt = IO.pipe
    IO.select([@_sleep_check], nil, nil, seconds)
  end

  def wakeup
    @_sleep_interrupt.close if @_sleep_interrupt && !@_sleep_interrupt.closed?
  end
end

class Test
  def initialize
    @pollers = []
    @sleepers = {}
  end

  def add_poller(poller, timeout = 1)
    @pollers << { poller: poller, timeout: timeout }
  end

  def start
    @running = true
    @threads = []
    @pollers.each { |poller| @threads << create_thread(poller[:poller], poller[:timeout]) }
    @threads.each(&:join)
  end

  def stop
    puts 'Shutting down pollers...'
    @running = false
    @sleepers.each_value(&:wakeup)
  end

  private

  def create_thread(poller, timeout)
    thread = Thread.new do
      thread_sid = Thread.current.object_id.to_s
      @sleepers[thread_sid] = InterruptibleSleep.new
      puts "Poller: #{poller}, Thread ID: #{Thread.current.object_id} initialized..."
      puts ''
      loop do
        @sleepers[thread_sid].sleep(timeout)
        puts "Poller: #{poller}, Thread ID: #{Thread.current.object_id}, running: #{@running}"
        puts ''
        break unless @running
      end
    end
    sleep 0.05
    thread
  end
end

test = Test.new

test.add_poller('5', 5)
test.add_poller('10', 10)
test.add_poller('20', 20)
test.add_poller('30', 30)

Signal.trap('SIGINT') { test.stop }

test.start

@aaronjensen
Copy link

aaronjensen commented Feb 6, 2018

Fwiw, the pattern here with the IO pipes appears to break on macs when they wake up from sleep. I don't know what causes it, but the app will get stuck reading from the pipe indefinitely and need to be killed. This is a different way of accomplishing the same thing that does not use pipes:

class InterruptibleSleeper
  def sleep(seconds)
    @sleeper = Thread.current
    Kernel.sleep seconds
  end

  def interrupt_sleep
    Thread.new { @sleeper&.run }
  end
end

This assumes that the thread that started the sleeping is still alive always, but additional safeguards could be put in place if that's an iffy thing.

This relies on the fact that Thread#run wakes threads up from sleep if another thread tells them to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment