ruby thread safety first

55
Text ! THREAD SAFETY FIRST Emily Stolfo Ruby Engineer at , Adjunct Faculty at Columbia @EmStolfo

Upload: emstolfo

Post on 25-Jun-2015

1.799 views

Category:

Technology


0 download

DESCRIPTION

We rubyists historically haven’t been in the habit of thinking about concurrency but the reality is that our thread-unsafe code often works by sheer luck. There are different implementations of Ruby with their own semantics that can unearth challenging and unexpected concurrency bugs in our code. We have to become more accustomed to writing threadsafe code in order to anticipate these potential surprises, especially in light of the rise in popularity of JRuby. I will discuss approaches to writing threadsafe code in this talk, with a specific focus on performance considerations and testing. I'll start by explaining some basic concurrency concepts, describe methods for handling shared mutable data, and touch on the subtleties of concurrency primitives (Mutex, ConditionVariable). Hair-raising, real-world bugs will be used throughout the presentation to illustrate specific concurrency issues and techniques for solving them.

TRANSCRIPT

Page 1: Ruby thread safety first

Text

!

THREADSAFETY

FIRST

Emily StolfoRuby Engineer at , Adjunct Faculty at Columbia@EmStolfo

Page 2: Ruby thread safety first

There is no such thing as thread-safe Ruby code.

Page 3: Ruby thread safety first

Ruby Engineer atAdjunct Faculty at ColumbiaExpert

Emily Stolfo

Page 4: Ruby thread safety first

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

Inconsistent bug?

2.0 with 200 threads

JRuby with 10 threads

JRuby with 200 threads

𝙭✔

Page 6: Ruby thread safety first

1. Concurrency in Ruby

! THREAD SAFETY FIRST

3. Testing concurrency

2. Writing thread-safe code

Page 7: Ruby thread safety first

CONCURRENCY IN RUBY

Page 8: Ruby thread safety first

There are different Ruby implementations.

Page 9: Ruby thread safety first

Threads are like music.

Page 10: Ruby thread safety first

GIL

green thread

native (OS) thread

Page 11: Ruby thread safety first

ruby 1.8

Threads and Ruby

Instruments: OS threadsNotes: green threadsConductor: GIL

Page 12: Ruby thread safety first

ruby 1.9+

Instruments: OS threadsNotes: green threadsConductor: GIL

Threads and Ruby

Page 13: Ruby thread safety first

JRuby

Instruments: OS threadsNotes: green threads

Threads and Ruby

Page 14: Ruby thread safety first

Different Rubies?Different semantics.

Page 15: Ruby thread safety first

Our code sometimes works by

sheer luck.

Page 16: Ruby thread safety first

You’re lucky your code hasn’t run on JRuby.

Page 17: Ruby thread safety first

You’re lucky there is a GIL.

Page 18: Ruby thread safety first

Example:

MongoDB Ruby driver and Replica Sets

Page 19: Ruby thread safety first

MongoDB Replica Set Creation

Page 20: Ruby thread safety first

MongoDB Replica Set

Page 21: Ruby thread safety first

MongoDB Replica Set Failure

Page 22: Ruby thread safety first

MongoDB Replica Set Recovery

Page 23: Ruby thread safety first

MongoDB Replica Set Recovered

Page 24: Ruby thread safety first

Mutable shared state.

Page 25: Ruby thread safety first

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

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

(RuntimeError) "can't add a new key into hash during iteration"

Page 28: Ruby thread safety first

We often use hashes as caches.

Page 29: Ruby thread safety first

Hashes and their derivatives are not

thread-safe in JRuby.

!

Page 30: Ruby thread safety first

How do we write this code thread-safely?

Page 31: Ruby thread safety first

WRITING THREAD-SAFE CODE

Page 32: Ruby thread safety first

Shared data:Avoid across threads.

Page 33: Ruby thread safety first

If you can’t avoid shared data, at least avoid

shared mutable data.

Page 34: Ruby thread safety first

If you can’t avoid shared mutable data,

use concurrency primitives.

Page 35: Ruby thread safety first

The top 2 concurrency primitives

Page 36: Ruby thread safety first

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

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

A mutex is not magic.

Page 39: Ruby thread safety first

Avoid locking around I/O.

Page 40: Ruby thread safety first

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

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

Consider resources.Ex: Thundering herd

Page 43: Ruby thread safety first

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

Consider your system.Ex: Queued requests

Appserver

Page 45: Ruby thread safety first

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

2. Condition Variable

Use to communicate between threads.

Page 47: Ruby thread safety first

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

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

TESTING CONCURRENCY

Page 50: Ruby thread safety first

Testing

1. Test with different implementations.

2. Test with a ton of threads.

3. Use patterns for precision.

Page 51: Ruby thread safety first

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

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

Use synchronization methods if you need

more precision.

ex: rendezvous / barrier pattern

Page 54: Ruby thread safety first

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

Thanks

Emily Stolfo@EmStolfo