Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Deprecated all clock-time based timer scheduling
- Only support scheduling by delay
- Effects `Concurrent.timer`, `TimerSet`, and `ScheduledTask`
* Added new `ReadWriteLock` class
* Consistent `at_exit` behavior for Java and Ruby thread pools.
* Added `at_exit` handler to Ruby thread pools (already in Java thread pools)
- Ruby handler stores the object id and retrieves from `ObjectSpace`
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ This library contains a variety of concurrency abstractions at high and low leve
* [M-Structures](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/MVar.html) (MVar)
* [Thread-local variables](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadLocalVar.html)
* [Software transactional memory](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/TVar.html) (TVar)
* [ReadWriteLock](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ReadWriteLock.html)

## Usage

Expand Down
139 changes: 139 additions & 0 deletions examples/benchmark_read_write_lock.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env ruby

$:.push File.join(File.dirname(__FILE__), '../lib')

require 'concurrent/atomic/read_write_lock'
require 'benchmark'
require 'optparse'
require 'ostruct'

$options = OpenStruct.new
$options.threads = 100
$options.interleave = false
$options.compare = false

OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"

opts.on('-t', '--threads=THREADS', OptionParser::DecimalInteger, "Number of threads per test (default #{$options.threads})") do |value|
$options.threads = value
end

opts.on('-i', '--[no-]interleave', 'Interleave output to check for starvation') do |value|
$options.interleave = value
end

opts.on('-c', '--[no-]compare', 'Compare with other implementations') do |value|
$options.compare = value
end

opts.on('-h', '--help', 'Prints this help') do
puts opts
exit
end
end.parse!

def jruby?
defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
end

# for performance comparison with ReadWriteLock
class SimpleMutex
def initialize; @mutex = Mutex.new; end
def with_read_lock
@mutex.synchronize { yield }
end
alias :with_write_lock :with_read_lock
end

# for seeing whether my correctness test is doing anything...
# and for seeing how great the overhead of the test is
# (apart from the cost of locking)
class FreeAndEasy
def with_read_lock
yield # thread safety is for the birds... I prefer to live dangerously
end
alias :with_write_lock :with_read_lock
end

if jruby?
# the Java platform comes with a read-write lock implementation
# performance is very close to ReadWriteLock, but just a *bit* slower
require 'java'
class JavaReadWriteLock
def initialize
@lock = java.util.concurrent.locks.ReentrantReadWriteLock.new
end
def with_read_lock
@lock.read_lock.lock
result = yield
@lock.read_lock.unlock
result
end
def with_write_lock
@lock.write_lock.lock
result = yield
@lock.write_lock.unlock
result
end
end
end

def test(lock)
puts "READ INTENSIVE (80% read, 20% write):"
single_test(lock, ($options.threads * 0.8).floor, ($options.threads * 0.2).floor)
puts "WRITE INTENSIVE (80% write, 20% read):"
single_test(lock, ($options.threads * 0.2).floor, ($options.threads * 0.8).floor)
puts "BALANCED (50% read, 50% write):"
single_test(lock, ($options.threads * 0.5).floor, ($options.threads * 0.5).floor)
end

def single_test(lock, n_readers, n_writers, reader_iterations=50, writer_iterations=50, reader_sleep=0.001, writer_sleep=0.001)
puts "Testing #{lock.class} with #{n_readers} readers and #{n_writers} writers. Readers iterate #{reader_iterations} times, sleeping #{reader_sleep}s each time, writers iterate #{writer_iterations} times, sleeping #{writer_sleep}s each time"
mutex = Mutex.new
bad = false
data = 0

result = Benchmark.measure do
readers = n_readers.times.collect do
Thread.new do
reader_iterations.times do
lock.with_read_lock do
print "r" if $options.interleave
mutex.synchronize { bad = true } if (data % 2) != 0
sleep(reader_sleep)
mutex.synchronize { bad = true } if (data % 2) != 0
end
end
end
end
writers = n_writers.times.collect do
Thread.new do
writer_iterations.times do
lock.with_write_lock do
print "w" if $options.interleave
# invariant: other threads should NEVER see "data" as an odd number
value = (data += 1)
# if a reader runs right now, this invariant will be violated
sleep(writer_sleep)
# this looks like a strange way to increment twice;
# it's designed so that if 2 writers run at the same time, at least
# one increment will be lost, and we can detect that at the end
data = value+1
end
end
end
end

readers.each { |t| t.join }
writers.each { |t| t.join }
puts "BAD!!! Readers+writers overlapped!" if mutex.synchronize { bad }
puts "BAD!!! Writers overlapped!" if data != (n_writers * writer_iterations * 2)
end
puts result
end

test(Concurrent::ReadWriteLock.new)
test(JavaReadWriteLock.new) if $options.compare && jruby?
test(SimpleMutex.new) if $options.compare
test(FreeAndEasy.new) if $options.compare
Loading