Created
December 11, 2011 11:08
-
-
Save ileitch/1459987 to your computer and use it in GitHub Desktop.
Interruptible sleep in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
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
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
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
To be able to call
interrupt_sleep
multiple times, I addedand !@_sleep_interrupt.closed?
to the condition: