1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
#!/usr/bin/env ruby
#$: << File.expand_path('../../lib', __FILE__)
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
|