Skip to content

Commit

Permalink
[GR-17457] Java monitor mixin
Browse files Browse the repository at this point in the history
PullRequest: truffleruby/2771
  • Loading branch information
aardvark179 committed Jul 26, 2021
2 parents 8ede413 + d94f3e9 commit 00b825c
Show file tree
Hide file tree
Showing 17 changed files with 361 additions and 115 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Compatibility:

Performance:

* Moved most of `MonitorMixin` to primitives to deal with interrupts more efficiently (#2375).

Changes:

Expand Down
15 changes: 15 additions & 0 deletions bench/micro/monitor/synchronize.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. This
# code is released under a tri EPL/GPL/LGPL license. You can use it,
# redistribute it and/or modify it under the terms of the:
#
# Eclipse Public License version 2.0, or
# GNU General Public License version 2, or
# GNU Lesser General Public License version 2.1.

require 'monitor'

m = Monitor.new
benchmark 'monitor-synchronize' do
m.synchronize do
end
end
122 changes: 33 additions & 89 deletions lib/mri/monitor.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# 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.
#
# truffleruby_primitives: true

# Copyright (c) 2015, 2020 Oracle and/or its affiliates. All rights reserved. This
# code is released under a tri EPL/GPL/LGPL license. You can use it,
# redistribute it and/or modify it under the terms of the:
#
# Eclipse Public License version 2.0, or
# GNU General Public License version 2, or
# GNU Lesser General Public License version 2.1.

# Original version licensed under LICENSE.RUBY as it is derived from
# lib/ruby/stdlib/digest.rb and is Copyright (C) 2001 Shugo Maeda
# <shugo@ruby-lang.org>

# 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
Expand Down Expand Up @@ -87,17 +91,14 @@
# MonitorMixin module.
#
module MonitorMixin
EXCEPTION_NEVER = {Exception => :never}.freeze
EXCEPTION_IMMEDIATE = {Exception => :immediate}.freeze

#
# 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
class Timeout < Exception; end

#
# Releases the lock held in the associated monitor and waits; reacquires the lock on wakeup.
Expand All @@ -106,18 +107,7 @@ class Timeout < Exception; end
# even if no other thread doesn't signal.
#
def wait(timeout = nil)
Thread.handle_interrupt(EXCEPTION_NEVER) do
@monitor.__send__(:mon_check_owner)
count = @monitor.__send__(:mon_exit_for_cond)
begin
Thread.handle_interrupt(EXCEPTION_IMMEDIATE) do
@cond.wait(@monitor.instance_variable_get(:@mon_mutex), timeout)
end
return true
ensure
@monitor.__send__(:mon_enter_for_cond, count)
end
end
@cond.wait(@mon_mutex, timeout)
end

#
Expand All @@ -142,23 +132,27 @@ def wait_until
# Wakes up the first thread in line waiting for this lock.
#
def signal
@monitor.__send__(:mon_check_owner)
check_owner
@cond.signal
end

#
# Wakes up all threads waiting for this lock.
#
def broadcast
@monitor.__send__(:mon_check_owner)
check_owner
@cond.broadcast
end

private

def initialize(monitor)
@monitor = monitor
@cond = Thread::ConditionVariable.new
def check_owner
raise ThreadError, "current thread not owner" unless @mon_mutex.owned?
end

def initialize(mutex)
@cond = ::ConditionVariable.new
@mon_mutex = mutex
end
end

Expand All @@ -171,15 +165,7 @@ def self.extend_object(obj)
# Attempts to enter exclusive section. Returns +false+ if lock fails.
#
def mon_try_enter
if @mon_owner != Thread.current
unless @mon_mutex.try_lock
return false
end
@mon_owner = Thread.current
@mon_count = 0
end
@mon_count += 1
return true
Primitive.monitor_try_enter(@mon_mutex)
end
# For backward compatibility
alias try_mon_enter mon_try_enter
Expand All @@ -188,24 +174,14 @@ def mon_try_enter
# Enters exclusive section.
#
def mon_enter
if @mon_owner != Thread.current
@mon_mutex.lock
@mon_owner = Thread.current
@mon_count = 0
end
@mon_count += 1
Primitive.monitor_enter(@mon_mutex)
end

#
# Leaves exclusive section.
#
def mon_exit
mon_check_owner
@mon_count -=1
if @mon_count == 0
@mon_owner = nil
@mon_mutex.unlock
end
Primitive.monitor_exit(@mon_mutex)
end

#
Expand All @@ -219,27 +195,16 @@ def mon_locked?
# Returns true if this monitor is locked by current thread.
#
def mon_owned?
@mon_mutex.locked? && @mon_owner == Thread.current
@mon_mutex.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
# Prevent interrupt on handling interrupts; for example timeout errors
# it may break locking state.
Thread.handle_interrupt(EXCEPTION_NEVER) do
mon_enter
begin
Thread.handle_interrupt(EXCEPTION_IMMEDIATE) do
yield
end
ensure
mon_exit
end
end
def mon_synchronize(&block)
Primitive.monitor_synchronize(@mon_mutex, block)
end
alias synchronize mon_synchronize

Expand All @@ -248,11 +213,9 @@ def mon_synchronize
# receiver.
#
def new_cond
return ConditionVariable.new(self)
ConditionVariable.new(@mon_mutex)
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.
Expand All @@ -264,32 +227,13 @@ def initialize(*args)
# 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_mutex) && @mon_mutex_owner_object_id == object_id
raise ThreadError, "already initialized"
if defined?(@mon_mutex) && Primitive.object_equal(@mon_mutex_owner_object, self)
raise ThreadError, 'already initialized'
end
@mon_mutex = Thread::Mutex.new
@mon_mutex_owner_object_id = object_id
@mon_owner = nil
@mon_count = 0
@mon_mutex_owner_object = self
end

def mon_check_owner
if @mon_owner != Thread.current
raise ThreadError, "current thread not owner"
end
end

def mon_enter_for_cond(count)
@mon_owner = Thread.current
@mon_count = count
end

def mon_exit_for_cond
count = @mon_count
@mon_owner = nil
@mon_count = 0
return count
end
end

# Use the Monitor class when you want to have a lock object for blocks with
Expand Down
28 changes: 28 additions & 0 deletions spec/ruby/library/monitor/enter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require_relative '../../spec_helper'
require 'monitor'

describe "Monitor#enter" do
it "acquires the monitor" do
monitor = Monitor.new
10.times do
wait_q = Queue.new
continue_q = Queue.new

thread = Thread.new do
begin
monitor.enter
wait_q << true
continue_q.pop
ensure
monitor.exit
end
end

wait_q.pop
monitor.mon_locked?.should == true
continue_q << true
thread.join
monitor.mon_locked?.should == false
end
end
end
88 changes: 88 additions & 0 deletions spec/ruby/library/monitor/new_cond_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require_relative '../../spec_helper'
require 'monitor'

describe "Monitor#new_cond" do
it "creates a MonitorMixin::ConditionVariable" do
m = Monitor.new
c = m.new_cond
c.class.should == MonitorMixin::ConditionVariable
end

it 'returns a condition variable which can be waited on by a thread holding the monitor' do
m = Monitor.new
c = m.new_cond

10.times do

wait_q = Queue.new
thread = Thread.new do
m.synchronize do
wait_q << true
c.wait
end
:done
end

wait_q.pop

# Synchronize can't happen until the other thread is waiting.
m.synchronize { c.signal }

thread.join
thread.value.should == :done
end
end

it 'returns a condition variable which can be waited on by a thread holding the monitor inside multiple synchronize blocks' do
m = Monitor.new
c = m.new_cond

10.times do

wait_q = Queue.new
thread = Thread.new do
m.synchronize do
m.synchronize do
wait_q << true
c.wait
end
end
:done
end

wait_q.pop

#No need to wait here as we cannot synchronize until the other thread is waiting.
m.synchronize { c.signal }

thread.join
thread.value.should == :done
end
end

it 'returns a condition variable which can be signalled by a thread holding the monitor inside multiple synchronize blocks' do
m = Monitor.new
c = m.new_cond

10.times do

wait_q = Queue.new
thread = Thread.new do
m.synchronize do
wait_q << true
c.wait
end
:done
end

wait_q.pop

# Synchronize can't happen until the other thread is waiting.
m.synchronize { m.synchronize { c.signal } }

thread.join
thread.value.should == :done
end
end

end
Loading

0 comments on commit 00b825c

Please sign in to comment.