Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Ruby thread safety first

2,891 views

Published on

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.

Published in: Technology

Ruby thread safety first

  1. 1. Text ! THREAD SAFETY FIRST Emily Stolfo Ruby Engineer at , Adjunct Faculty at Columbia @EmStolfo
  2. 2. There is no such thing as thread-safe Ruby code.
  3. 3. Ruby Engineer at Adjunct Faculty at Columbia Expert Emily Stolfo
  4. 4. require 'set' members = Set.new threads = [] 200.times do |n| threads << Thread.new do if n % 2 == 0 members << n else members.first.nil? end end end threads.each(&:join) Demo code
  5. 5. Inconsistent bug? 2.0 with 200 threads JRuby with 10 threads JRuby with 200 threads ✔ 𝙭 ✔
  6. 6. 1. Concurrency in Ruby ! THREAD SAFETY FIRST 3. Testing concurrency 2. Writing thread-safe code
  7. 7. CONCURRENCY IN RUBY
  8. 8. There are different Ruby implementations.
  9. 9. Threads are like music.
  10. 10. GIL green thread native (OS) thread
  11. 11. ruby 1.8 Threads and Ruby Instruments: OS threads Notes: green threads Conductor: GIL
  12. 12. ruby 1.9+ Instruments: OS threads Notes: green threads Conductor: GIL Threads and Ruby
  13. 13. JRuby Instruments: OS threads Notes: green threads Threads and Ruby
  14. 14. Different Rubies? Different semantics.
  15. 15. Our code sometimes works by sheer luck.
  16. 16. You’re lucky your code hasn’t run on JRuby.
  17. 17. You’re lucky there is a GIL.
  18. 18. Example: MongoDB Ruby driver and Replica Sets
  19. 19. MongoDB Replica Set Creation
  20. 20. MongoDB Replica Set
  21. 21. MongoDB Replica Set Failure
  22. 22. MongoDB Replica Set Recovery
  23. 23. MongoDB Replica Set Recovered
  24. 24. Mutable shared state.
  25. 25. 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 } end end Ruby driver
  26. 26. 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 } end end Potential concurrency bug
  27. 27. (RuntimeError) "can't add a new key into hash during iteration"
  28. 28. We often use hashes as caches.
  29. 29. Hashes and their derivatives are not thread-safe in JRuby. !
  30. 30. How do we write this code thread-safely?
  31. 31. WRITING THREAD-SAFE CODE
  32. 32. Shared data: Avoid across threads.
  33. 33. If you can’t avoid shared data, at least avoid shared mutable data.
  34. 34. If you can’t avoid shared mutable data, use concurrency primitives.
  35. 35. The top 2 concurrency primitives
  36. 36. 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 end end 1. Mutex
  37. 37. 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 end end 1. Mutex Use to isolate code that should be executed by at most one thread at a time. shared replica set state update
  38. 38. A mutex is not magic.
  39. 39. Avoid locking around I/O.
  40. 40. 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 end end 1. Mutex network I/O
  41. 41. 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 end end 1. Mutex network I/O
  42. 42. Consider resources. Ex: Thundering herd
  43. 43. Thundering herd n threads are woken up after waiting on something, but only 1 can continue. Waste of system resources to wake up n threads.
  44. 44. Consider your system. Ex: Queued requests App server
  45. 45. 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. App server
  46. 46. 2. Condition Variable Use to communicate between threads.
  47. 47. 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 end end Condition Variable broadcast Why is this a waste of system resources?
  48. 48. 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 end end Condition Variable signal Only one thread can continue
  49. 49. TESTING CONCURRENCY
  50. 50. Testing 1. Test with different implementations. 2. Test with a ton of threads. 3. Use patterns for precision.
  51. 51. require 'set' members = Set.new threads = [] 10.times do |n| threads << Thread.new do if n % 2 == 0 members << n else members.first.nil? end end end threads.each(&:join) Test with a ton of threads.
  52. 52. require 'set' members = Set.new threads = [] 200.times do |n| threads << Thread.new do if n % 2 == 0 members << n else members.first.nil? end end end threads.each(&:join) Test with a ton of threads.
  53. 53. Use synchronization methods if you need more precision. ex: rendezvous / barrier pattern
  54. 54. Concurrency in Ruby Know your implementations. ! Know your concurrency primitives. ! Know your code. ”thread-safe JRuby 1.7.4 code” !
  55. 55. Thanks Emily Stolfo @EmStolfo

×