Skip to content

Synchronization ExecutorService #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 3, 2015
Merged

Conversation

jdantonio
Copy link
Member

Creates a new ExecutorService and related series of modules and classes through which all executors use the new synchronization. Additionally:

@jdantonio
Copy link
Member Author

@pitr-ch This PR incorporates your suggestions. I quickly made the updates before work this morning so I expect I probably missed much.

@jdantonio jdantonio added this to the 0.9.0 Release milestone Apr 28, 2015
@jdantonio jdantonio self-assigned this Apr 28, 2015
@pitr-ch
Copy link
Member

pitr-ch commented Apr 29, 2015

@jdantonio Should I review or will you be working on it more first?

@jdantonio
Copy link
Member Author

@pitr-ch I consider this a spike and not ready to merge, but please provide your thoughts. I will be working on it more this evening. I prefer this approach over the other two spikes but we should explicitly choose one of the three approaches. I also want to incorporate the updates @headius suggested in #280 and incorporate #284 once it's been merged to master. I was unable to fully review #284 last night (I gave a presentation to a small group of students at the University of Akron) but I plan to review it more fully today. My initial impression is that #284 should go into master first.

synchronize do
@stop_event = Event.new
@stopped_event = Event.new
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you wait for #284 you can use ns_initialize here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction you can consider those two fields final so:

def initialize
  super()
  @StopEvent    = Event.new
  @StoppedEvent = Event.new
  ensure_ivar_visibility!
end

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRs #284 and #285 will be first, then I'll rebase and update this branch.

@jdantonio jdantonio force-pushed the synchronization-executor-service branch from 5d8dc0f to 3c32888 Compare May 2, 2015 13:26
@jdantonio
Copy link
Member Author

The last update implements the performance improvements from #280. The performance difference is significant:

Before:

$ ./examples/benchmark_thread_pool_implementations.rb
Rehearsal ----------------------------------------------------------------------
Concurrent::JavaThreadPoolExecutor  12.200000   0.820000  13.020000 (  4.115000)
------------------------------------------------------------ total: 13.020000sec

                                         user     system      total        real
Concurrent::JavaThreadPoolExecutor   8.040000   0.320000   8.360000 (  2.734000)

After:

$ ./examples/benchmark_thread_pool_implementations.rb
Rehearsal ----------------------------------------------------------------------
Concurrent::JavaThreadPoolExecutor   2.750000   0.320000   3.070000 (  0.951000)
------------------------------------------------------------- total: 3.070000sec

                                         user     system      total        real
Concurrent::JavaThreadPoolExecutor   0.610000   0.060000   0.670000 (  0.339000)

# is not running
#
# @raise [ArgumentError] if no task is given
def post(*args, &task)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, what kind of args would you imagine being sent to a task?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any values needed within the task can be passed via args. Ruby has closures so any values needed within the task can be accessed via the enclosing context. Doing so, however, requires the runtime to retain the entire closure. This may be inefficient if the closure is large and only a few values are needed. Passing data as arguments instead will allow the runtime to release the closure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes that's what I was imagining. Good call. We should document this behavior somewhere as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also another danger. The access to closure's variables is not synchronized (on JRuby, based on documentation) which may lead to issues since the task is executed on different thread. The args should be protected against that to avoid this issue, e.g. Job object with final fields.

@jdantonio do you know about any sources talking about the closure releasing? I would like to know more. I always assumed that the closure is held because of following situation for example:

[1] pry(main)> a = 1
=> 1
[2] pry(main)> proc { local_variables }
=> #<Proc:0x007f21885d3850@(pry):2>
[4] pry(main)> proc { |s| eval s }.call 'local_variables'
=> [:s, :a, :__, :_, :_dir_, :_file_, :_ex_, :_pry_, :_out_, :_in_]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitr-ch The issue of closures came up a long time ago in a conversation another PR or Issue. Someone had written a (very favorable) blog post in which it was noted that arguments couldn't be passed into one of our abstractions. I can't remember which one (I want to say it was Future, but I honestly can't remember). At issue was relying on the closure vs. a scoped variable. I don't remember the details. The result of the conversation was that I added a feature to allow argument passing. I'll see if I can find the Issue/PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ianks @pitr-ch This is the discussion where this came up: #223

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ianks @pitr-ch Now that I've read #223 again I recall that the issue wasn't related to releasing the closure, it was due to variable scope.

@jdantonio
Copy link
Member Author

JRuby Executor Performance

See #280

Old Implementation

jruby-1.7.18

Rehearsal ------------------------------------------------------------------------
Concurrent::JavaCachedThreadPool      11.990000   1.380000  13.370000 (  8.653000)
Concurrent::JavaFixedThreadPool        8.400000   0.990000   9.390000 (  6.394000)
Concurrent::JavaSingleThreadExecutor   6.870000   0.100000   6.970000 (  4.003000)
Concurrent::JavaThreadPoolExecutor     6.590000   0.170000   6.760000 (  5.311000)
-------------------------------------------------------------- total: 36.490000sec

                                           user     system      total        real
Concurrent::JavaCachedThreadPool       7.410000   1.140000   8.550000 (  5.772000)
Concurrent::JavaFixedThreadPool        7.250000   1.020000   8.270000 (  5.242000)
Concurrent::JavaSingleThreadExecutor   4.910000   0.430000   5.340000 (  3.919000)
Concurrent::JavaThreadPoolExecutor     3.850000   0.670000   4.520000 (  3.792000)

jruby-head

Rehearsal ------------------------------------------------------------------------
Concurrent::JavaCachedThreadPool      10.860000   1.270000  12.130000 (  6.242069)
Concurrent::JavaFixedThreadPool        8.030000   0.880000   8.910000 (  4.917759)
Concurrent::JavaSingleThreadExecutor   6.160000   0.290000   6.450000 (  3.971956)
Concurrent::JavaThreadPoolExecutor     4.220000   0.570000   4.790000 (  4.240146)
-------------------------------------------------------------- total: 32.280000sec

                                           user     system      total        real
Concurrent::JavaCachedThreadPool       4.150000   0.840000   4.990000 (  4.100592)
Concurrent::JavaFixedThreadPool        3.860000   0.830000   4.690000 (  3.580397)
Concurrent::JavaSingleThreadExecutor   3.840000   0.630000   4.470000 (  3.355992)
Concurrent::JavaThreadPoolExecutor     3.760000   0.560000   4.320000 (  3.441682)

New Implementation

jruby-1.7.18

Rehearsal ------------------------------------------------------------------------
Concurrent::JavaCachedThreadPool       2.980000   0.840000   3.820000 (  1.982000)
Concurrent::JavaFixedThreadPool        1.330000   0.450000   1.780000 (  0.919000)
Concurrent::JavaSingleThreadExecutor   0.570000   0.060000   0.630000 (  0.437000)
Concurrent::JavaThreadPoolExecutor     0.750000   0.270000   1.020000 (  0.761000)
--------------------------------------------------------------- total: 7.250000sec

                                           user     system      total        real
Concurrent::JavaCachedThreadPool       1.080000   0.680000   1.760000 (  1.038000)
Concurrent::JavaFixedThreadPool        0.920000   0.620000   1.540000 (  0.915000)
Concurrent::JavaSingleThreadExecutor   0.590000   0.340000   0.930000 (  0.543000)
Concurrent::JavaThreadPoolExecutor     0.970000   0.250000   1.220000 (  0.693000)

jruby-head

Rehearsal ------------------------------------------------------------------------
Concurrent::JavaCachedThreadPool       3.460000   0.880000   4.340000 (  2.240732)
Concurrent::JavaFixedThreadPool        1.040000   0.260000   1.300000 (  0.666024)
Concurrent::JavaSingleThreadExecutor   0.760000   0.080000   0.840000 (  0.516168)
Concurrent::JavaThreadPoolExecutor     0.520000   0.150000   0.670000 (  0.429587)
--------------------------------------------------------------- total: 7.150000sec

                                           user     system      total        real
Concurrent::JavaCachedThreadPool       0.750000   0.190000   0.940000 (  0.681543)
Concurrent::JavaFixedThreadPool        0.820000   0.710000   1.530000 (  0.902916)
Concurrent::JavaSingleThreadExecutor   0.390000   0.150000   0.540000 (  0.328649)
Concurrent::JavaThreadPoolExecutor     0.460000   0.300000   0.760000 (  0.602376)

@jdantonio
Copy link
Member Author

I believe this PR is ready to merge. The failing test is the Condition test that has been causing us problems on Rbx.

@@ -55,14 +55,14 @@ public IRubyObject initialize(ThreadContext context, IRubyObject[] args, Block b
}
}

@JRubyMethod(name = "synchronize", visibility = Visibility.PRIVATE)
@JRubyMethod(name = "synchronize", visibility = Visibility.PROTECTED)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious what is behind this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With those methods private the subclasses didn't work. Every call to synchronize on JRuby raised an exception.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show me the synchronize calls that were failing? Sounds like we may have a visibility-checking bug somewhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@headius Change line 58 to Visibility.PRIVATE and run the specs. Or simply load up IRB and use any class that subclasses Synchronization::Object. You will get NoMethodError: private methodsynchronize' called for...any time a subclass calls the#synchronizemethod. Which, from where I'm sitting, is the expected behavior. Private visibility should prevent subclasses from calling a method on a parent whereas protected visibility allows subclasses to access the method. TheSynchronization::Objectclass [extends](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/synchronization/object.rb) theSynchronization::JavaObject` class so protected is the correct visibility--given the way we structure things internally.

Since Delay is synchronized and we use it to lazy-load the global thread pools, you'll never get the gem to load if you change line 58 above. Commit 0181005 is a quick-and-dirty update which changes the visibility of JavaObject and changes the visibility of synchronize back to protected withinDelay. On that branch if you try to create anIVarwithinbundle console` you'll get this:

jruby-1.7.19 :001 > require 'concurrent'
 => true
jruby-1.7.19 :002 > ivar = Concurrent::IVar.new
NoMethodError: private method `synchronize' called for #<Concurrent::IVar:0x490caf5f>
    from /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/dereferenceable.rb:87:in `set_deref_options'
    from /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/ivar.rb:154:in `ns_initialize'
    from com/concurrent_ruby/ext/SynchronizationLibrary.java:54:in `initialize'
    from /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/ivar.rb:64:in `initialize'
    from (irb):2:in `evaluate'
    from org/jruby/RubyKernel.java:1107:in `eval'
    from org/jruby/RubyKernel.java:1507:in `loop'
    from org/jruby/RubyKernel.java:1270:in `catch'
    from org/jruby/RubyKernel.java:1270:in `catch'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/cli/console.rb:14:in `run'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/cli.rb:300:in `console'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/vendor/thor/lib/thor.rb:359:in `dispatch'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/vendor/thor/lib/thor/base.rb:440:in `start'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/cli.rb:10:in `start'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/bin/bundle:20:in `(root)'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/lib/bundler/friendly_errors.rb:7:in `with_friendly_errors'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/bundler-1.9.6/bin/bundle:18:in `(root)'
    from org/jruby/RubyKernel.java:1087:in `load'
    from /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/bin/bundle:23:in `(root)'jruby-1.7.19 :003 >
jruby-1.7.19 :004 >

On master if you run the specs you get this:

$ rspec spec/concurrent/delay_spec.rb
/Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/simplecov-0.9.2/lib/simplecov.rb:31 warning: tracing (e.g. set_trace_func) will not capture all events without --debug flag
NoMethodError: private method `synchronize' called for #<Concurrent::Delay:0x4abf3f0>
     set_deref_options at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/dereferenceable.rb:87
         ns_initialize at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/delay.rb:167
            initialize at com/concurrent_ruby/ext/SynchronizationLibrary.java:54
            initialize at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/delay.rb:72
            initialize at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/utility/processor_count.rb:8
            Concurrent at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/utility/processor_count.rb:143
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/utility/processor_count.rb:4
               require at org/jruby/RubyKernel.java:1071
               require at /Users/jdantonio/.rvm/rubies/jruby-1.7.19/lib/ruby/shared/rubygems/core_ext/kernel_require.rb:54
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/configuration.rb:1
               require at org/jruby/RubyKernel.java:1071
               require at /Users/jdantonio/.rvm/rubies/jruby-1.7.19/lib/ruby/shared/rubygems/core_ext/kernel_require.rb:54
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent/configuration.rb:6
               require at org/jruby/RubyKernel.java:1071
               require at /Users/jdantonio/.rvm/rubies/jruby-1.7.19/lib/ruby/shared/rubygems/core_ext/kernel_require.rb:54
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent.rb:1
               require at org/jruby/RubyKernel.java:1071
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/lib/concurrent.rb:6
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/spec/spec_helper.rb:1
                  each at org/jruby/RubyArray.java:1613
                (root) at /Users/jdantonio/Projects/opensource/concurrent-ruby/spec/spec_helper.rb:23
                (root) at /Users/jdantonio/.rvm/rubies/jruby-1.7.19/lib/ruby/shared/rubygems/core_ext/kernel_require.rb:1
                  each at org/jruby/RubyArray.java:1613
               require at /Users/jdantonio/.rvm/rubies/jruby-1.7.19/lib/ruby/shared/rubygems/core_ext/kernel_require.rb:54
             requires= at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/rspec-core-3.2.3/lib/rspec/core/configuration.rb:1181
             requires= at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/rspec-core-3.2.3/lib/rspec/core/configuration.rb:1181
  process_options_into at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/rspec-core-3.2.3/lib/rspec/core/configuration_options.rb:110
  process_options_into at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/rspec-core-3.2.3/lib/rspec/core/configuration_options.rb:109
             configure at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/rspec-core-3.2.3/lib/rspec/core/configuration_options.rb:22
                 setup at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/gems/rspec-core-3.2.3/lib/rspec/core/runner.rb:96
                  load at org/jruby/RubyKernel.java:1087
                (root) at /Users/jdantonio/.rvm/gems/jruby-1.7.19@concurrent-ruby/bin/rspec:23

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdantonio it's counter intuitive but private methods can be actually called from sub-classes in Ruby

A = Class.new { private def a; :a; end }; B = Class.new(A); B.new.instance_eval {a} # => :a

It seemed weird to me too, I should have thought about it more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calls you point out are both from within a module, calling synchronize against @mutex, which is presumably the IVar above. I suspect JRuby has a problem with visibility from module methods. Thanks for the info.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I am not sure about this code now. I was unable to get a module to dispatch to a private method even on a hierarchy that had included it. Further, I was unable to get a dotted form (foo.bar) to dispatch to a parent's private methods. Only the bare version worked:

[] ~/projects/jruby $ rvm ruby-2.2 do ruby -e "class A; private def bar; end; end; class B < A; def foo; bar; end; end; B.new.foo"

[] ~/projects/jruby $ rvm ruby-2.2 do ruby -e "class A; private def bar; end; end; class B < A; def foo; self.bar; end; end; B.new.foo"
-e:1:in `foo': private method `bar' called for #<B:0x007fc681927300> (NoMethodError)
    from -e:1:in `<main>'

[] ~/projects/jruby $ rvm ruby-2.2 do ruby -e "class A; private def bar; end; end; class B < A; def foo; (a = self; a).bar; end; end; B.new.foo"
-e:1:in `foo': private method `bar' called for #<B:0x007fe82a0df218> (NoMethodError)
    from -e:1:in `<main>'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pitr-ch All these years using Ruby and I never realize that subclasses could access private methods of superclasses--simply because I've never tried it. Your phrase "counter intuitive" is very polite. I like the word "dumb" better. That's not what protected is supposed to mean! I'll bring that up with Matz the next time he and I chat. :-) Seriously, though, I had to write some code and run it myself. Sadly, it worked exactly as you described...

@headius Ruby is as Ruby does, I guess. Thanks for taking the time to look into it but for now we'll just keep it protected. Behavior aside, I think protected is semantically and logically correct for what we're doing.

@pitr-ch
Copy link
Member

pitr-ch commented May 3, 2015

👍

jdantonio added a commit that referenced this pull request May 3, 2015
…r-service

Synchronization ExecutorService
@jdantonio jdantonio merged commit de789ad into master May 3, 2015
@jdantonio jdantonio deleted the synchronization-executor-service branch May 3, 2015 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants