aboutsummaryrefslogtreecommitdiffstats
path: root/ext
diff options
context:
space:
mode:
authorKoichi Sasada <ko1@atdot.net>2019-10-20 04:52:20 +0900
committerGitHub <noreply@github.com>2019-10-20 04:52:20 +0900
commitcaac5f777ae288b5982708b8690e712e1cae0cf6 (patch)
treee4257d65c062b7e8c9e4c4b962cee3ca7c5e1d4c /ext
parent434966bffddd4299d34f5d1f7f225bf7396d0807 (diff)
downloadruby-caac5f777ae288b5982708b8690e712e1cae0cf6.tar.gz
make monitor.so for performance. (#2576)
Recent monitor.rb has performance problem because of interrupt handlers. 'Monitor#synchronize' is frequently used primitive so the performance of this method is important. This patch rewrite 'monitor.rb' with 'monitor.so' (C-extension) and make it faster. See [Feature #16255] for details. Monitor class objects are normal object which include MonitorMixin. This patch introduce a Monitor class which is implemented on C and MonitorMixin uses Monitor object as re-entrant (recursive) Mutex. This technique improve performance because we don't need to care atomicity and we don't need accesses to instance variables any more on Monitor class.
Diffstat (limited to 'ext')
-rw-r--r--ext/monitor/depend13
-rw-r--r--ext/monitor/extconf.rb2
-rw-r--r--ext/monitor/lib/monitor.rb287
-rw-r--r--ext/monitor/monitor.c189
4 files changed, 491 insertions, 0 deletions
diff --git a/ext/monitor/depend b/ext/monitor/depend
new file mode 100644
index 0000000000..89efe1766b
--- /dev/null
+++ b/ext/monitor/depend
@@ -0,0 +1,13 @@
+# AUTOGENERATED DEPENDENCIES START
+monitor.o: $(RUBY_EXTCONF_H)
+monitor.o: $(arch_hdrdir)/ruby/config.h
+monitor.o: $(hdrdir)/ruby/assert.h
+monitor.o: $(hdrdir)/ruby/backward.h
+monitor.o: $(hdrdir)/ruby/defines.h
+monitor.o: $(hdrdir)/ruby/intern.h
+monitor.o: $(hdrdir)/ruby/missing.h
+monitor.o: $(hdrdir)/ruby/ruby.h
+monitor.o: $(hdrdir)/ruby/st.h
+monitor.o: $(hdrdir)/ruby/subst.h
+monitor.o: monitor.c
+# AUTOGENERATED DEPENDENCIES END
diff --git a/ext/monitor/extconf.rb b/ext/monitor/extconf.rb
new file mode 100644
index 0000000000..78c53fa0c5
--- /dev/null
+++ b/ext/monitor/extconf.rb
@@ -0,0 +1,2 @@
+require 'mkmf'
+create_makefile('monitor')
diff --git a/ext/monitor/lib/monitor.rb b/ext/monitor/lib/monitor.rb
new file mode 100644
index 0000000000..dba942c89a
--- /dev/null
+++ b/ext/monitor/lib/monitor.rb
@@ -0,0 +1,287 @@
+# frozen_string_literal: false
+# = monitor.rb
+#
+# Copyright (C) 2001 Shugo Maeda <shugo@ruby-lang.org>
+#
+# This library is distributed under the terms of the Ruby license.
+# You can freely distribute/modify this library.
+#
+
+#
+# In concurrent programming, a monitor is an object or module intended to be
+# used safely by more than one thread. The defining characteristic of a
+# monitor is that its methods are executed with mutual exclusion. That is, at
+# each point in time, at most one thread may be executing any of its methods.
+# This mutual exclusion greatly simplifies reasoning about the implementation
+# of monitors compared to reasoning about parallel code that updates a data
+# structure.
+#
+# You can read more about the general principles on the Wikipedia page for
+# Monitors[http://en.wikipedia.org/wiki/Monitor_%28synchronization%29]
+#
+# == Examples
+#
+# === Simple object.extend
+#
+# require 'monitor.rb'
+#
+# buf = []
+# buf.extend(MonitorMixin)
+# empty_cond = buf.new_cond
+#
+# # consumer
+# Thread.start do
+# loop do
+# buf.synchronize do
+# empty_cond.wait_while { buf.empty? }
+# print buf.shift
+# end
+# end
+# end
+#
+# # producer
+# while line = ARGF.gets
+# buf.synchronize do
+# buf.push(line)
+# empty_cond.signal
+# end
+# end
+#
+# The consumer thread waits for the producer thread to push a line to buf
+# while <tt>buf.empty?</tt>. The producer thread (main thread) reads a
+# line from ARGF and pushes it into buf then calls <tt>empty_cond.signal</tt>
+# to notify the consumer thread of new data.
+#
+# === Simple Class include
+#
+# require 'monitor'
+#
+# class SynchronizedArray < Array
+#
+# include MonitorMixin
+#
+# def initialize(*args)
+# super(*args)
+# end
+#
+# alias :old_shift :shift
+# alias :old_unshift :unshift
+#
+# def shift(n=1)
+# self.synchronize do
+# self.old_shift(n)
+# end
+# end
+#
+# def unshift(item)
+# self.synchronize do
+# self.old_unshift(item)
+# end
+# end
+#
+# # other methods ...
+# end
+#
+# +SynchronizedArray+ implements an Array with synchronized access to items.
+# This Class is implemented as subclass of Array which includes the
+# MonitorMixin module.
+#
+
+require 'monitor.so'
+
+module MonitorMixin
+ #
+ # FIXME: This isn't documented in Nutshell.
+ #
+ # Since MonitorMixin.new_cond returns a ConditionVariable, and the example
+ # above calls while_wait and signal, this class should be documented.
+ #
+ class ConditionVariable
+ #
+ # Releases the lock held in the associated monitor and waits; reacquires the lock on wakeup.
+ #
+ # If +timeout+ is given, this method returns after +timeout+ seconds passed,
+ # even if no other thread doesn't signal.
+ #
+ def wait(timeout = nil)
+ @monitor.mon_check_owner
+ count = @monitor.__send__(:exit_for_cond)
+ begin
+ @cond.wait(@monitor.__send__(:mutex_for_cond), timeout)
+ return true
+ ensure
+ @monitor.__send__(:enter_for_cond, count)
+ end
+ end
+
+ #
+ # Calls wait repeatedly while the given block yields a truthy value.
+ #
+ def wait_while
+ while yield
+ wait
+ end
+ end
+
+ #
+ # Calls wait repeatedly until the given block yields a truthy value.
+ #
+ def wait_until
+ until yield
+ wait
+ end
+ end
+
+ #
+ # Wakes up the first thread in line waiting for this lock.
+ #
+ def signal
+ @monitor.mon_check_owner
+ @cond.signal
+ end
+
+ #
+ # Wakes up all threads waiting for this lock.
+ #
+ def broadcast
+ @monitor.mon_check_owner
+ @cond.broadcast
+ end
+
+ private
+
+ def initialize(monitor)
+ @monitor = monitor
+ @cond = Thread::ConditionVariable.new
+ end
+ end
+
+ def self.extend_object(obj)
+ super(obj)
+ obj.__send__(:mon_initialize)
+ end
+
+ #
+ # Attempts to enter exclusive section. Returns +false+ if lock fails.
+ #
+ def mon_try_enter
+ @mon_data.try_enter
+ end
+ # For backward compatibility
+ alias try_mon_enter mon_try_enter
+
+ #
+ # Enters exclusive section.
+ #
+ def mon_enter
+ @mon_data.enter
+ end
+
+ #
+ # Leaves exclusive section.
+ #
+ def mon_exit
+ mon_check_owner
+ @mon_data.exit
+ end
+
+ #
+ # Returns true if this monitor is locked by any thread
+ #
+ def mon_locked?
+ @mon_data.mon_locked?
+ end
+
+ #
+ # Returns true if this monitor is locked by current thread.
+ #
+ def mon_owned?
+ @mon_data.mon_owned?
+ end
+
+ #
+ # Enters exclusive section and executes the block. Leaves the exclusive
+ # section automatically when the block exits. See example under
+ # +MonitorMixin+.
+ #
+ def mon_synchronize(&b)
+ @mon_data.enter
+ begin
+ yield
+ ensure
+ @mon_data.exit
+ end
+ end
+ alias synchronize mon_synchronize
+
+ #
+ # Creates a new MonitorMixin::ConditionVariable associated with the
+ # receiver.
+ #
+ def new_cond
+ return ConditionVariable.new(@mon_data)
+ end
+
+ private
+
+ # Use <tt>extend MonitorMixin</tt> or <tt>include MonitorMixin</tt> instead
+ # of this constructor. Have look at the examples above to understand how to
+ # use this module.
+ def initialize(*args)
+ super
+ mon_initialize
+ end
+
+ # Initializes the MonitorMixin after being included in a class or when an
+ # object has been extended with the MonitorMixin
+ def mon_initialize
+ if defined?(@mon_data) && @mon_data_owner_object_id == self.object_id
+ raise ThreadError, "already initialized"
+ end
+ @mon_data = ::Monitor.new
+ @mon_data_owner_object_id = self.object_id
+ end
+
+ def mon_check_owner
+ @mon_data.mon_check_owner
+ end
+end
+
+# Use the Monitor class when you want to have a lock object for blocks with
+# mutual exclusion.
+#
+# require 'monitor'
+#
+# lock = Monitor.new
+# lock.synchronize do
+# # exclusive access
+# end
+#
+class Monitor
+ def new_cond
+ ::MonitorMixin::ConditionVariable.new(self)
+ end
+
+ # for compatibility
+ alias try_mon_enter try_enter
+ alias mon_try_enter try_enter
+ alias mon_enter enter
+ alias mon_exit exit
+ alias mon_synchronize synchronize
+end
+
+# Documentation comments:
+# - All documentation comes from Nutshell.
+# - MonitorMixin.new_cond appears in the example, but is not documented in
+# Nutshell.
+# - All the internals (internal modules Accessible and Initializable, class
+# ConditionVariable) appear in RDoc. It might be good to hide them, by
+# making them private, or marking them :nodoc:, etc.
+# - RDoc doesn't recognise aliases, so we have mon_synchronize documented, but
+# not synchronize.
+# - mon_owner is in Nutshell, but appears as an accessor in a separate module
+# here, so is hard/impossible to RDoc. Some other useful accessors
+# (mon_count and some queue stuff) are also in this module, and don't appear
+# directly in the RDoc output.
+# - in short, it may be worth changing the code layout in this file to make the
+# documentation easier
diff --git a/ext/monitor/monitor.c b/ext/monitor/monitor.c
new file mode 100644
index 0000000000..cf9e7fe07d
--- /dev/null
+++ b/ext/monitor/monitor.c
@@ -0,0 +1,189 @@
+#include "ruby/ruby.h"
+
+/* Thread::Monitor */
+
+struct rb_monitor {
+ long count;
+ const VALUE owner;
+ const VALUE mutex;
+};
+
+static void
+monitor_mark(void *ptr)
+{
+ struct rb_monitor *mc = ptr;
+ rb_gc_mark(mc->owner);
+ rb_gc_mark(mc->mutex);
+}
+
+static size_t
+monitor_memsize(const void *ptr)
+{
+ return sizeof(struct rb_monitor);
+}
+
+static const rb_data_type_t monitor_data_type = {
+ "monitor",
+ {monitor_mark, RUBY_TYPED_DEFAULT_FREE, monitor_memsize,},
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY|RUBY_TYPED_WB_PROTECTED
+};
+
+static VALUE
+monitor_alloc(VALUE klass)
+{
+ struct rb_monitor *mc;
+ VALUE obj;
+
+ obj = TypedData_Make_Struct(klass, struct rb_monitor, &monitor_data_type, mc);
+ RB_OBJ_WRITE(obj, &mc->mutex, rb_mutex_new());
+ RB_OBJ_WRITE(obj, &mc->owner, Qnil);
+ mc->count = 0;
+
+ return obj;
+}
+
+static struct rb_monitor *
+monitor_ptr(VALUE monitor)
+{
+ struct rb_monitor *mc;
+ TypedData_Get_Struct(monitor, struct rb_monitor, &monitor_data_type, mc);
+ return mc;
+}
+
+static int
+mc_owner_p(struct rb_monitor *mc)
+{
+ return mc->owner == rb_thread_current();
+}
+
+static VALUE
+monitor_try_enter(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+
+ if (!mc_owner_p(mc)) {
+ if (!rb_mutex_trylock(mc->mutex)) {
+ return Qfalse;
+ }
+ RB_OBJ_WRITE(monitor, &mc->owner, rb_thread_current());
+ mc->count = 0;
+ }
+ mc->count += 1;
+ return Qtrue;
+}
+
+static VALUE
+monitor_enter(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ if (!mc_owner_p(mc)) {
+ rb_mutex_lock(mc->mutex);
+ RB_OBJ_WRITE(monitor, &mc->owner, rb_thread_current());
+ mc->count = 0;
+ }
+ mc->count++;
+ return Qnil;
+}
+
+static VALUE
+monitor_exit(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ mc->count--;
+ if (mc->count == 0) {
+ RB_OBJ_WRITE(monitor, &mc->owner, Qnil);
+ rb_mutex_unlock(mc->mutex);
+ }
+ return Qnil;
+}
+
+static VALUE
+monitor_locked_p(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ return rb_mutex_locked_p(mc->mutex);
+}
+
+static VALUE
+monitor_owned_p(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ return (rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc)) ? Qtrue : Qfalse;
+}
+
+static VALUE
+monitor_check_owner(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ if (!mc_owner_p(mc)) {
+ rb_raise(rb_eThreadError, "current thread not owner");
+ }
+ return Qnil;
+}
+
+static VALUE
+monitor_enter_for_cond(VALUE monitor, VALUE count)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ RB_OBJ_WRITE(monitor, &mc->owner, rb_thread_current());
+ mc->count = NUM2LONG(count);
+ return Qnil;
+}
+
+static VALUE
+monitor_exit_for_cond(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ long cnt = mc->count;
+ RB_OBJ_WRITE(monitor, &mc->owner, Qnil);
+ mc->count = 0;
+ return LONG2NUM(cnt);
+}
+
+static VALUE
+monitor_mutex_for_cond(VALUE monitor)
+{
+ struct rb_monitor *mc = monitor_ptr(monitor);
+ return mc->mutex;
+}
+
+static VALUE
+monitor_sync_body(VALUE monitor)
+{
+ return rb_yield_values(0);
+}
+
+static VALUE
+monitor_sync_ensure(VALUE monitor)
+{
+ return monitor_exit(monitor);
+}
+
+static VALUE
+monitor_synchronize(VALUE monitor)
+{
+ monitor_enter(monitor);
+ return rb_ensure(monitor_sync_body, monitor, monitor_sync_ensure, monitor);
+}
+
+void
+Init_monitor(void)
+{
+ VALUE rb_cMonitor = rb_define_class("Monitor", rb_cObject);
+ rb_define_alloc_func(rb_cMonitor, monitor_alloc);
+
+ rb_define_method(rb_cMonitor, "try_enter", monitor_try_enter, 0);
+ rb_define_method(rb_cMonitor, "enter", monitor_enter, 0);
+ rb_define_method(rb_cMonitor, "exit", monitor_exit, 0);
+ rb_define_method(rb_cMonitor, "synchronize", monitor_synchronize, 0);
+
+ /* internal methods for MonitorMixin */
+ rb_define_method(rb_cMonitor, "mon_locked?", monitor_locked_p, 0);
+ rb_define_method(rb_cMonitor, "mon_check_owner", monitor_check_owner, 0);
+ rb_define_method(rb_cMonitor, "mon_owned?", monitor_owned_p, 0);
+
+ /* internal methods for MonitorMixin::ConditionalVariable */
+ rb_define_private_method(rb_cMonitor, "enter_for_cond", monitor_enter_for_cond, 1);
+ rb_define_private_method(rb_cMonitor, "exit_for_cond", monitor_exit_for_cond, 0);
+ rb_define_private_method(rb_cMonitor, "mutex_for_cond", monitor_mutex_for_cond, 0);
+}