0

I started on Ruby less than a week ago but have already come to appreciate the power of the language. I am trying my hands on a classic producer-consumer problem, implemented as an Orange tree (c.f. http://pine.fm/LearnToProgram/?Chapter=09). The Orange tree grows each year until it dies and produces a random number of Oranges each year (Producer). Oranges can be picked as long there are any on the tree (Consumer).

I've got two problems here:

  1. The following code gives me the following exception (can't attach, no option):

    /Users/Abhijit/Workspace/eclipse/ruby/learn_to_program/orange_tree.rb:84:
    warning: instance variable @orange_tree not initialized
    /Users/Abhijit/Workspace/eclipse/ruby/learn_to_program/orange_tree.rb:84:in `':
    
    undefined method `age' for nil:NilClass (NoMethodError) from
    /Users/Abhijit/Workspace/eclipse/ruby/learn_to_program/orange_tree.rb:45:in `'
    
  2. I am not sure that the multithreading part is correctly coded.

I've got myself a couple of books, including "Programming Ruby" and "The Ruby Programming Language", but none of them contain a true "producer-consumer problem".

P.S: For the sake of full disclosure, I've also posted this question in the Ruby forum. However, I have seen excellent answers and/or suggestions provided here and hope that I'd get some of those too.

require 'thread'

class OrangeTree
GROWTH_PER_YEAR = 1
AGE_TO_START_PRODUCING_ORANGE = 3
AGE_TO_DIE = 7
ORANGE_COUNT_RELATIVE_TO_AGE = 50
def initialize
  @height = 0
  @age = 0
  @orange_count = 0
end

def height
  return @height
end

def age
  return @age
end

def count_the_oranges
  return @orange_count
end

def one_year_passes
  @age += 1
  @height += GROWTH_PER_YEAR
  @orange_count = Math.rand(@age..AGE_TO_DIE) * Math.log(@age) * ORANGE_COUNT_RELATIVE_TO_AGE
end

def pick_an_orange
  if (@age == AGE_TO_DIE)
    puts "Sorry, the Orange tree is dead"
  elsif (@orange_count > 0)
    @orange_count -= 1
    puts "The Orange is delicious"
  else
    puts "Sorry, no Oranges to pick"
  end
end

end

class Worker
  def initialize(mutex, cv, orange_tree)
  @mutex = mutex
  @cv = cv
  @orange_tree = orange_tree
end

def do_some_work
  Thread.new do
    until (@orange_tree.age == OrangeTree.AGE_TO_DIE)
      @mutex.synchronize do
        sleep_time = rand(0..5)
        puts "Orange picker going to sleep for #{sleep_time}"
        sleep(sleep_time)
        puts "Orange picker woke up after sleeping for #{sleep_time}"
        @orange_tree.pick_an_orange
        puts "Orange picker waiting patiently..."
        @cv.wait(@mutex)
      end
    end
  end

  Thread.new do
    until (@orange_tree.age == OrangeTree.AGE_TO_DIE)
      @mutex.synchronize do
        sleep_time = rand(0..5)
        puts "Age increaser going to sleep for #{sleep_time}"
        sleep(sleep_time)
        puts "Age increaser woke up after sleeping for #{sleep_time}"
        @orange_tree.one_year_passes
        puts "Age increaser increased the age"
        @cv.signal
      end
    end
  end
end

Worker.new(Mutex.new, ConditionVariable.new, OrangeTree.new).do_some_work
until (@orange_tree.age == OrangeTree.AGE_TO_DIE)
  # wait for the Threads to finish
end

end
Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
Abhijit Sarkar
  • 21,927
  • 20
  • 110
  • 219

1 Answers1

0

@orange_tree is an instance variable of the Worker object and may only be accessed from inside the object. You're trying to access it from the global scope in your "until" condition, where it doesn't exist. There are a few ways to address it, but this one requires the fewest changes:

...

orange_tree = OrangeTree.new
Worker.new(Mutex.new, ConditionVariable.new, orange_tree).do_some_work
until (orange_tree.age == OrangeTree::AGE_TO_DIE)
...

You should also look into Thread#join: http://www.ruby-doc.org/core-1.9.3/Thread.html#method-i-join. It it will take care of the waiting for you. The rest of the multithreading code looks technically correct. I'm sure you know that if this were more than an exercise, you would want to make the mutexes more granular. As they are now the two threads will execute nearly mutually exclusively which kind of defeats the point of threads.

Also, look up attr_accessor or attr_reader. They will save you having to manually define OrangeTree#height, age, etc.

Multiple join example

threads = []

threads << Thread.new do
  ...
end

threads << Threads.new do
  ...
end

threads.each { |thread| thread.join }
bioneuralnet
  • 5,283
  • 1
  • 24
  • 30
  • After making the changes you suggested, including join, I am now getting: Orange picker going to sleep for 5 Orange picker woke up after sleeping for 5 Sorry, no Oranges to pick Orange picker waiting patiently... Age increaser going to sleep for 2 Age increaser woke up after sleeping for 2 /Users/Abhijit/Workspace/eclipse/ruby/learn_to_program/orange_tree.rb:76:in `join': deadlock detected (fatal) from /Users/Abhijit/Workspace/eclipse/ruby/learn_to_program/orange_tree.rb:76:in `do_some_work' from... – Abhijit Sarkar Feb 14 '13 at 12:25
  • Is that on the second `join`? Does it look like the example above? – bioneuralnet Feb 14 '13 at 16:07
  • Here's some code snippet (wish I knew how to format it) end orange_picker.join age_increaser.join end worker = Worker.new(Mutex.new,ConditionVariable.new,OrangeTree.new) worker.do_some_work end – Abhijit Sarkar Feb 14 '13 at 16:47
  • That looks fine. Debugging deadlocks can be difficult. I would start by commenting out everything in the two threads that you possibly can. If that "fixes" the deadlock, then uncomment lines one-by-one and see which line is causing it. (I obviously can't see what `@cv.wait` and `@cv.signal` are doing, but I have a hunch the problem is there.) – bioneuralnet Feb 14 '13 at 17:34
  • I rewrote the code using MonitorMixin and then back to using Mutex, same result. I used 2 ConditionVariable, same result :( If it's not working, it's certainly not for the lack of trying. – Abhijit Sarkar Feb 15 '13 at 03:09