Ruby thread safety first
-
Upload
emstolfo -
Category
Technology
-
view
1.800 -
download
0
description
Transcript of Ruby thread safety first
![Page 1: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/1.jpg)
Text
!
THREADSAFETY
FIRST
Emily StolfoRuby Engineer at , Adjunct Faculty at Columbia@EmStolfo
![Page 2: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/2.jpg)
There is no such thing as thread-safe Ruby code.
![Page 3: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/3.jpg)
Ruby Engineer atAdjunct Faculty at ColumbiaExpert
Emily Stolfo
![Page 4: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/4.jpg)
require 'set'
members = Set.newthreads = []
200.times do |n| threads << Thread.new do if n % 2 == 0 members << n else members.first.nil? end endend
threads.each(&:join)
Demo code
![Page 5: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/5.jpg)
Inconsistent bug?
2.0 with 200 threads
JRuby with 10 threads
JRuby with 200 threads
✔
𝙭✔
![Page 6: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/6.jpg)
1. Concurrency in Ruby
! THREAD SAFETY FIRST
3. Testing concurrency
2. Writing thread-safe code
![Page 7: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/7.jpg)
CONCURRENCY IN RUBY
![Page 8: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/8.jpg)
There are different Ruby implementations.
![Page 9: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/9.jpg)
Threads are like music.
![Page 10: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/10.jpg)
GIL
green thread
native (OS) thread
![Page 11: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/11.jpg)
ruby 1.8
Threads and Ruby
Instruments: OS threadsNotes: green threadsConductor: GIL
![Page 12: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/12.jpg)
ruby 1.9+
Instruments: OS threadsNotes: green threadsConductor: GIL
Threads and Ruby
![Page 13: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/13.jpg)
JRuby
Instruments: OS threadsNotes: green threads
Threads and Ruby
![Page 14: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/14.jpg)
Different Rubies?Different semantics.
![Page 15: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/15.jpg)
Our code sometimes works by
sheer luck.
![Page 16: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/16.jpg)
You’re lucky your code hasn’t run on JRuby.
![Page 17: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/17.jpg)
You’re lucky there is a GIL.
![Page 18: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/18.jpg)
Example:
MongoDB Ruby driver and Replica Sets
![Page 19: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/19.jpg)
MongoDB Replica Set Creation
![Page 20: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/20.jpg)
MongoDB Replica Set
![Page 21: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/21.jpg)
MongoDB Replica Set Failure
![Page 22: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/22.jpg)
MongoDB Replica Set Recovery
![Page 23: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/23.jpg)
MongoDB Replica Set Recovered
![Page 24: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/24.jpg)
Mutable shared state.
![Page 25: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/25.jpg)
class ReplicaSetClient def initialize @nodes = Set.new end
...
def connect_to_nodes seed = valid_node seed.node_list.each do |host| node = Node.new(host) @nodes << node if node.connect end end
def choose_node(type)@nodes.detect { |n| n.type == type }
endend
Ruby driver
![Page 26: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/26.jpg)
class ReplicaSetClient def initialize @nodes = Set.new end
...
def connect_to_nodes seed = valid_node seed.node_list.each do |host| node = Node.new(host) @nodes << node if node.connect end end
def choose_node(type)@nodes.detect { |n| n.type == type }
endend
Potential concurrency bug
![Page 27: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/27.jpg)
(RuntimeError) "can't add a new key into hash during iteration"
![Page 28: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/28.jpg)
We often use hashes as caches.
![Page 29: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/29.jpg)
Hashes and their derivatives are not
thread-safe in JRuby.
!
![Page 30: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/30.jpg)
How do we write this code thread-safely?
![Page 31: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/31.jpg)
WRITING THREAD-SAFE CODE
![Page 32: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/32.jpg)
Shared data:Avoid across threads.
![Page 33: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/33.jpg)
If you can’t avoid shared data, at least avoid
shared mutable data.
![Page 34: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/34.jpg)
If you can’t avoid shared mutable data,
use concurrency primitives.
![Page 35: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/35.jpg)
The top 2 concurrency primitives
![Page 36: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/36.jpg)
class ReplicaSetClient def initialize @nodes = Set.new @connect_mutex = Mutex.new end
...
def connect_to_nodes seed = valid_node seed.node_list.each do |host| node = Node.new(host) @connect_mutex.synchronize do @nodes << node if node.connect end end end
def choose_node(type) @connect_mutex.synchronize do @nodes.detect { |n| n.type == type } end endend
1. Mutex
![Page 37: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/37.jpg)
class ReplicaSetClient def initialize @nodes = Set.new @connect_mutex = Mutex.new end
...
def connect_to_nodes seed = valid_node seed.node_list.each do |host| node = Node.new(host) @connect_mutex.synchronize do @nodes << node if node.connect end end end
def choose_node(type) @connect_mutex.synchronize do @nodes.detect { |n| n.type == type } end endend
1. MutexUse to isolate code
that should be executed by at most one thread at a time.
sharedreplica
setstate
update
![Page 38: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/38.jpg)
A mutex is not magic.
![Page 39: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/39.jpg)
Avoid locking around I/O.
![Page 40: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/40.jpg)
class ReplicaSetClient def initialize @nodes = Set.new @connect_mutex = Mutex.new end
...
def connect_to_nodes seed = valid_node seed.node_list.each do |host| node = Node.new(host) @connect_mutex.synchronize do @nodes << node if node.connect end end end
def choose_node(type) @connect_mutex.synchronize do @nodes.detect { |n| n.type == type } end endend
1. Mutex
network I/O
![Page 41: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/41.jpg)
class ReplicaSetClient def initialize @nodes = Set.new @connect_mutex = Mutex.new end
...
def connect_to_nodes seed = valid_node seed.node_list.each do |host| node = Node.new(host) @connect_mutex.synchronize do @nodes << node end if node.connect end end
def choose_node(type) @connect_mutex.synchronize do @nodes.detect { |n| n.type == type } end endend
1. Mutex
networkI/O
![Page 42: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/42.jpg)
Consider resources.Ex: Thundering herd
![Page 43: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/43.jpg)
Thundering herdn threads are woken up after waiting on something, but only 1 can continue. Waste of system resources
to wake up n threads.
![Page 44: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/44.jpg)
Consider your system.Ex: Queued requests
Appserver
![Page 45: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/45.jpg)
Think systemically.n threads are waiting to write to the replica set
primary. The primary is flooded with requests when the replica set state is refreshed.
Appserver
![Page 46: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/46.jpg)
2. Condition Variable
Use to communicate between threads.
![Page 47: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/47.jpg)
class Pool def initialize @socket_available = ConditionVariable.new ... end
def checkout loop do @lock.synchronize do if @available_sockets.size > 0 socket = @available_sockets.next return socket end @socket_available.wait(@lock) end end end
def checkin(socket) @lock.synchronize do @available_sockets << socket @socket_available. end endend
Condition Variable
broadcast
Why is this a waste of
system resources?
![Page 48: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/48.jpg)
class Pool def initialize @socket_available = ConditionVariable.new ... end
def checkout loop do @lock.synchronize do if @available_sockets.size > 0 socket = @available_sockets.next return socket end @socket_available.wait(@lock) end end end
def checkin(socket) @lock.synchronize do @available_sockets << socket @socket_available. end endend
Condition Variable
signal
Only one thread
can continue
![Page 49: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/49.jpg)
TESTING CONCURRENCY
![Page 50: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/50.jpg)
Testing
1. Test with different implementations.
2. Test with a ton of threads.
3. Use patterns for precision.
![Page 51: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/51.jpg)
require 'set'
members = Set.newthreads = []
10.times do |n| threads << Thread.new do if n % 2 == 0 members << n else members.first.nil? end endend
threads.each(&:join)
Test with a ton of threads.
![Page 52: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/52.jpg)
require 'set'
members = Set.newthreads = []
200.times do |n| threads << Thread.new do if n % 2 == 0 members << n else members.first.nil? end endend
threads.each(&:join)
Test with a ton of threads.
![Page 53: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/53.jpg)
Use synchronization methods if you need
more precision.
ex: rendezvous / barrier pattern
![Page 54: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/54.jpg)
Concurrency in Ruby
Know your implementations.
!
Know your concurrency primitives.!
Know your code.”thread-safe JRuby 1.7.4 code”
!
![Page 55: Ruby thread safety first](https://reader035.fdocuments.us/reader035/viewer/2022062319/558c027bd8b42acc5b8b45ea/html5/thumbnails/55.jpg)
Thanks
Emily Stolfo@EmStolfo