[QUIZ] Negative Sleep (#87)

I just checked rubyquiz.com for some submissions and realized that sending
my submission as tar.bz2 was, errr, not the best idea.
Maybe it would be a good idea to add a warning about it somewhere or did I
overlook it?
Anyway here is my submission again. I was doing some rather havy
synchronisation stuff which is probably unneccessary, I would appreciate any
suggestions. I have the feeling that just using Thread.critical might be
enough,otoh I would interfere with threading libraries which want to use my
sleep-.

Here ya go

Definition

···

==========

While sleep(n) [n > 0] assures that the calling Thread sleeps for at least n
seconds,
unless awoken by #run, sleep(m) [m < 0] assures that the calling Thread will
suffer from
insomnia for at least -m seconds, unless resting with #stop or #sleep(n) [n

0]

As we know sleep(n) [n > 0] returns the time really slept, sleep(m) [m < 0]
cannot easily do
such a thing as it returns immediately. But when we put ourselves out of
insomnia with Thread#stop
we can return the time we were in insomnia. Thread#stop will still return
nil if the calling thread
was not in insomnia.

Implementation

Instead of rewriting the scheduler I will try to give an implementation by
using Thread
priorities.
A thread that calls sleep(m) [m < 0] will be assigned the highest priority
of all threads. It also starts
another thread, called supervisor with even higher priority. But the
supervisor just sleeps for -m
seconds before reassigning the normal priority to the calling thread.

The Thread is in "insomnia" mode during this time span. As mentioned above
there are only two
ways to get out of this "insomnia" mode. By the timeout -m, or if the
calling thread calls sleep(n)
[n > 0] or stop.

In order to do that we have to intercept all calls to Kernel#sleep and to
methods
adjusting a Thread's priority, which are:
    * Thread#new
    * Thread#fork
    * Thread#priority=
    * Thread#stop
to get out of insomnia ourselves.

I will ignore Thread#new and Thread#fork assuming that they initialize a
Thread with priority 0.

Testing

running test.rb with a parameter of >> 10**5 should give a first insight of
how it works, depending
on the speed of your machine.

Shortcomings

* The naming is quite trivial and might conflict with subclasses of Thread.
* The implementation is minimal, an insomnia Thread does not get back into
insomnia mode after
    a sleep(n) [n > 0] as one might assume. An insomnia thread cannot call
sleep(m) [m < 0] again
    as might be useful.
* When calling sleep before synchronizing the Thread might be interrupted
and thus the effect
    of sleep(m) [m < 0] might not be immideate (as can be seen in test2.rb).
*Synchronization is done with big guns and without optimization, the former
because
    I do not know ruby Threads well, the later because of clarity of code.

* And hopefully *you* tell me about the other ones ;).

----------------------- 8< --------------------------------------
#!/usr/bin/ruby

require 'thread'

class IllegalMonitorState < RuntimeError ; end

class BusyFlag # taken from Scott Oak's and Henry Wong's "Java Threads"
    # allows to be called again for a Thread
    # already in possession of the Flag
    def initialize
        @count = 0
        @possessor = nil
        @mutex = Mutex.new
        @cv = ConditionVariable.new
    end

    def get
        @mutex.synchronize do
            while not try_get do
                @cv.wait( @mutex )
            end
        end
    end

    def free
        @mutex.synchronize do
            return unless possessing?
            @count -= 1
            return unless @count.zero?
            @possessor = nil
            @cv.signal
        end
    end

    private
    def possessing?
        Thread.current == @possessor
    end

    def try_get
        if @possessor.nil? then
            @possessor = Thread.current
            @count = 1
            return true
        end
        return false unless possessing?
        @count += 1
        true
    end

end

class Thread
    ### keep track if there is a thread in insomnia by means of a supervisor
    ### control the insomnia with a supervisor
    @@supervisor = nil
    ### time the supervisor slept
    @@time = 0
    ### keep track of the maximum prioriy
    @@max_prio = Thread.current.priority + 1
    ### synchronizing management data
    @@lock = BusyFlag.new

    alias_method :orig_priority=, :priority=
    def priority= np
        # I do not think this needs to be protected, if a thread is
interrupted while setting the priority
        # it cannot yet run with that priority and well not interfere with
the insomnia process.
        @@max_prio = np if np > @@max_prio
        self.orig_priority = np
    end

    class << self

        alias_method :orig_stop, :stop

        def insomnia?; @@supervisor end
        def max_prio; @@max_prio end
        def last_time; @@time end

        def lock; @@lock; end

        def set_insomnia n
            synchronize do
                old_prio = Thread.current.priority
                Thread.current.orig_priority = @@max_prio + 1
                @@supervisor = Thread.new( Thread.current ) {
                    >t>
                    Thread.current.orig_priority = @@max_prio + 2
                    @@time = orig_sleep n
                    t.orig_priority = old_prio
                }
                @@supervisor = nil
            end
        end

        def stop_insomnia
            @@supervisor.run if @@supervisor
        end

        def stop
            synchronize do
                stop_insomnia
                orig_stop
                @@time
            end
        end

        def synchronize( &block )
            begin
                @@lock.get
                block.call
            ensure
                @@lock.free
            end
        end
    end
end

module Kernel
    alias_method :orig_sleep, :sleep

    def sleep n
        Thread.synchronize do
            if n < 0 then
                raise IllegalMonitorState, "Already in insommnia mode" if
Thread.insomnia?
                return Thread.set_insomnia( -n )
            end
            Thread.lock.free
            ### if we are in Thread insomnia we might be interrupted by the
supervisor which gets us out of it
            ### but even if we call Thread.stop_insomnia then that has no
effect. So no sync needed :slight_smile:
            Thread.stop_insomnia if Thread.insomnia?
            orig_sleep n
        end
    end
end

Hi there, Ruby Quiz! Nice to meet you. It's my first time here, so don't
be too
rude with me :smiley:

Err.. well.. here is my solution to the negative sleep issue.
I've been highly inspired by MenTaLguY's

  ( Computation.new { ... } + Sleep.new(-1) + Computation.new { ... } ).run

It executes the second computation one second before the first.
But I thought the code was a bit redundant. Ruby already has a
"Computation"-thing, doesn't it?
I just extended Proc to be able to do something like:

  (proc { ... } + ProcStack.sleep(-1) + proc { ... }).call

Looks cleaner IMO.

ProcStack.sleep(-1) returns more or less a "proc { sleep 1 }" with a
little meta-information appended which
informs our ProcStack-instance (created by Proc#+) that the sleep as
well as the following proc
should be executed before the previous proc. I hope you all
understood... my english isn't the best
and even in my native language (german, btw) I suck at explaining things.

Gah, just read the code. ^^

···

#-------------------------------------------------------------------------------
# time machine, whee!

class NegativeProc < Proc; end
class ProcStack
  def initialize(*args)
    @negative = args.shift if args.first == true
    @stack = args
  end

  def + code
    new_stack = @stack.dup

    if code.is_a? NegativeProc
      new_stack.insert(-2, code)
      new_stack.unshift(true)
    elsif code.respond_to? 'call'
      if @negative
        new_stack.insert(-3, code)
      else
        new_stack.push(code)
      end
    end

    ProcStack.new(*new_stack)
  end

  def call
    @stack.each { |p| p.call }
  end

  def ProcStack.sleep(time)
    if time < 0
      NegativeProc.new { Kernel.sleep(time.abs) }
    else
      Proc.new { Kernel.sleep(time) }
    end
  end
end

class Proc
  def + code
    ProcStack.new(self) + code
  end
end

# should print something like "chunky ... bacon\hooray for foxes"
whee = proc { print "bacon\n" } + ProcStack.sleep(-2) + proc { print
"chunky " } +
       ProcStack.sleep(1) +
       proc { print "hooray for " } + proc { print "foxes" }
whee.call

#-------------------------------------------------------------------------------

Well.. I know the solution has weaknesses and could be extended.
(This @negative-thingy is quite an ugly hack, but I don't see a better
way ATM)
Also I have something like free movable code in my mind:

  proc { omg } + proc(-1) { wtf } + proc(1) { bbq } + proc { lol }

which would evaluate to:

  wtf; omg; lol; bbq

Would be funny to maintain such source, wouldn't it?
But since the current code does all work asked for in the quiz I'm
satisfied with it.

~dingsi

The mailing list link requires a little work to extract it from, yes. (I was to do that as a quiz someday.) However, I'm pretty diligent. I have your code and it will be uploaded with the summary on Thursday. No worries. :wink:

James Edward Gray II

···

On Jul 18, 2006, at 4:56 PM, Robert Dober wrote:

I just checked rubyquiz.com for some submissions and realized that sending
my submission as tar.bz2 was, errr, not the best idea.
Maybe it would be a good idea to add a warning about it somewhere or did I
overlook it?

Robert

···

On 7/19/06, James Edward Gray II <james@grayproductions.net> wrote:

On Jul 18, 2006, at 4:56 PM, Robert Dober wrote:

> I just checked rubyquiz.com for some submissions and realized that
> sending
> my submission as tar.bz2 was, errr, not the best idea.
> Maybe it would be a good idea to add a warning about it somewhere
> or did I
> overlook it?

The mailing list link requires a little work to extract it from,
yes. (I was to do that as a quiz someday.) However, I'm pretty
diligent. I have your code and it will be uploaded with the summary
on Thursday. No worries. :wink:

James Edward Gray II

Well thank you, I will not do it again :wink: