File: benchmark_read_write_lock.rb

package info (click to toggle)
ruby-concurrent 1.1.6%2Bdfsg-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 30,284 kB
  • sloc: ruby: 30,875; java: 6,117; ansic: 288; makefile: 9; sh: 6
file content (139 lines) | stat: -rwxr-xr-x 4,374 bytes parent folder | download | duplicates (4)
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