On 28.09.2010 11:16, Alex Stahl wrote:
> No prob... but I'm not sure it's quite what you're looking for.  I'm
> refactoring code I can't get to work instead of finding the root cause
> in the threading.  (BTW - any thread insight would be appreciated based
> on this write-up - cuz there's still problems!  I'm starting to consider
> that a Threaded Load Tester Gem might be handy...)
>
> After staring at the screen for too long, I took a break to ponder
> whether the organization of my threads was fundamentally flawed.  It
> occurred to me that one of the problems I was having - a deadlock -
> could be avoided by instantiating the threads only when needed.
>
> If you review my original code snippet, I spin up all the threads w/ a
> proc object in initialize(), then .stop them, then perform via method
> a .call on each element of the thread's api array, then a .wakeup
> followed by the .join.
>
> ..
> 1.upto @count do
>    @threads<<  Thread.new do
>      Thread.current[:hi5] = Hi5fbapi.new 'redacted params'
>      Thread.current["api_calls"] = []
>      apis.each do |api|
>        Thread.current["api_calls"]<<  get_call(api) #pushes a proc obj
>      end
>      Thread.stop
>    end
> end
> ..
> @threads.each do |thr|
>    thr[:api_calls].each do |api|
>      p api.call thr[:hi5], @session
>    end
> end
> @threads.each {|t| t.wakeup.join}
> ..
>
> On 1.8.7 w/ a single core processor, this is a highly deterministic
> sequence, and would not deadlock.  Once deployed to a multi-core VM
> running 1.9.2-p0 (selected specifically for concurrency), not so much.
>
> There I encountered more deadlocks and also "NoMethodError"s from
> Sinatra (undefined method `bytesize' for #<Thread:0xa36ae0c dead>).
> This would occur during the .each where I would .join.  So it's trying
> to join a 'dead' thread.  Except that if I add anything to prevent that,
> such as
>
> .. unless t.status == 'dead'...
>
> it would still deadlock or NoMethodError.
>
> But further testing showed that adding any operation to the main thread,
> prior to calling .join, would prevent the deadlock:
>
> ..
> @threads.each do |thr|
>    p thr.inspect
> ..
>
> Anyway, I rewrote things where the threads are created in the method,
> not initialize(), so '@api_calls' is already populated by procs, and it
> works fine at small scale:
>
> ..
> thr = []
> 1.upto @count do
>    thr<<  Thread.new do
>      @api_calls.each do |api|
>        p api.call @hi5, @session
>        Thread.pass
>      end
>    end
> end
> thr.each {|t| t.join}
> ..
>
> This runs fine on 1.8.7/single and 1.9.2/multi at like 5-10 threads.
> But when I ramp up to, say, 5,000 (it is a *load* test!), 1.8.7 is fine
> but 1.9.2 segfaults.
>
> Even 500 threads on 1.9.2 is segfaulting right now (but not 1.8.7).  I
> get the handy output:
>
> [NOTE]
> You may have encountered a bug in the Ruby interpreter or extension
> libraries.
> Bug reports are welcome.
> For details: http://www.ruby-lang.org/bugreport.html
>
> I'll write it up tomorrow.
>
> Some open questions:
> 1. When .join is called on a multi-core system, what qualifies as the
> calling thread?  The .main on the main processor, or the thread which
> instantiates my ThreadedLoadTester object?  (i.e. what if
> ThreadedLoadTester is created from a sinatra thread which itself isn't
> main?)
>
> 2. Sinatra regularly reports a 'NoMethodError' for 'bytesize' when the
> last thread is dead but joined to the main thread.  But only when the
> main thread originates w/in sinatra, and not an inline call.
>
> 3. Is there a theoretical maximum to the number of concurrent threads
> which can be created which all access a network interface?  This is
> admittedly a poor theory - what might really cause a segfault in 1.9.2
> when 500 threads all try to access the network?

Without going into too much detail I believe one flaw of your design 
here is that you are not using thread synchronization but instead try to 
explicitly start and stop threads and yield execution.  It may be that 
this is causing your cores, but I really don't know.

What I would do:

1. Use a condition variable to let all threads start at the same time.

2. use Thread#value to collect results.

require 'thread'

lock = Mutex.new
cond = ConditionVariable.new
start = false

threads = (1..10).map do
   Thread.new do
     lock.synchronize do
       until start
         cond.wait(lock)
       end
     end

     # work
     # return results
     [rand(10), rand(100)]
   end
end

lock.synchronize do
   start = true
   cond.signal
end

threads.each do |th|
   p th.value
end

You can probably get away without the condition variable by just 
acquiring the lock (lock.synchronize) in the main thread before you 
create all threads and let all threads just synchronize with an empty block.

Kind regards

	robert


-- 
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/